text: /**
   * A class containing utility functions for use with jsonapi-vuex
   *
   * **Note** - an instance of this class is exported as `utils` from `jsonapi-vuex`,
   * so it does not need to be used directly.
   *
   * @name Utils
   * @namespace utils
   * @memberof module:jsonapi-vuex
   * @param {object} conf - A jsonapi-vuex config object.
   */
  
  import Vue from 'vue';
  import get from 'lodash.get';
  import isEqual from 'lodash.isequal';
  import merge from 'lodash.merge';
  
  /**
   * Helper methods added to `_jv` by {@link module:jsonapi-vuex.utils.addJvHelpers}
   * @namespace helpers
   * @memberof module:jsonapi-vuex.
   */
  
  /**
   * Documentation for internal functions etc.
   * These are not available when the module is imported,
   * and are documented for module developers only.
   * @namespace _internal
   * @memberof module:jsonapi-vuex
   */
  
  const Utils = class {
    constructor(conf) {
      this.conf = conf;
      this.jvtag = conf.jvtag;
      this.context = null;
    }
  
    /**
     * @memberof module:jsonapi-vuex._internal
     * @param {object} data - An object to be deep copied
     * @return {object} A deep copied object
     */
  
    _copy(data) {
      // Recursive object copying function (for 'simple' objects)
      const out = Array.isArray(data) ? [] : {};
      for (const key in data) {
        // null is typeof 'object'
        if (typeof data[key] === 'object' && data[key] !== null) {
          out[key] = this._copy(data[key]);
        } else {
          out[key] = data[key];
        }
      }
      return out;
    }
  
    /**
     * Add helper functions and getters to a restructured object
     * @memberof module:jsonapi-vuex.utils
     * @param {object} obj - An object to assign helpers to
     * @return {object} A copy of the object with added helper functions/getters
     */
    addJvHelpers(obj) {
      // Avoid 'this' confusion in property definitions
      const { jvtag } = this;
      const { hasProperty } = this;
      if (obj[jvtag] && !hasProperty(obj[jvtag], 'isRel') && !hasProperty(obj[jvtag], 'isAttr')) {
        Object.assign(obj[jvtag], {
          /**
           * @memberof module:jsonapi-vuex.helpers
           * @param {string} name - Name of a (potential) relationship
           * returns {boolean} true if the name given is a relationship of this object
           */
          isRel(name) {
            return hasProperty(get(obj, [jvtag, 'relationships'], {}), name);
          },
          /**
           * @memberof module:jsonapi-vuex.helpers
           * @param {string} name - Name of a (potential) attribute
           * returns {boolean} true if the name given is an attribute of this object
           */
          isAttr(name) {
            return name !== jvtag && hasProperty(obj, name) && !obj[jvtag].isRel(name);
          },
        });
      }
      /**
       * @memberof module:jsonapi-vuex.helpers
       * @name rels
       * @property {object} - An object containing all relationships for this object
       */
      if (hasProperty(obj, jvtag)) {
        Object.defineProperty(obj[jvtag], 'rels', {
          get() {
            const rel = {};
            for (const key of Object.keys(get(obj, [jvtag, 'relationships'], {}))) {
              rel[key] = obj[key];
            }
            return rel;
          },
          // Allow to be redefined
          configurable: true,
        });
        /**
         * @memberof module:jsonapi-vuex.helpers
         * @name attrs
         * @property {object} - An object containing all attributes for this object
         */
        Object.defineProperty(obj[jvtag], 'attrs', {
          get() {
            const att = {};
            for (const [key, val] of Object.entries(obj)) {
              if (obj[jvtag].isAttr(key)) {
                att[key] = val;
              }
            }
            return att;
          },
          // Allow to be redefined
          configurable: true,
        });
      }
      return obj;
    }
  
    /**
     * If `followRelationshipData` is set, call `followRelationships` for either an item or a collection
     * See {@link module:jsonapi-vuex~Configuration|Configuration}
     * @memberof module:jsonapi-vuex._internal
     * @param {object} state - Vuex state object
     * @param {object} getters - Vuex getters object
     * @param {object} records - Record(s) to follow relationships for.
     * @param {array} seen - internal recursion state-tracking
     * @return {object} records with relationships followed
     */
    checkAndFollowRelationships(state, getters, records, seen) {
      if (this.conf.followRelationshipsData) {
        let resData = {};
        if (this.hasProperty(records, this.jvtag)) {
          // single item
          resData = this.followRelationships(state, getters, records, seen);
        } else {
          // multiple items
          for (const [key, item] of Object.entries(records)) {
            resData[key] = this.followRelationships(state, getters, item, seen);
          }
        }
        if (resData) {
          return resData;
        }
      }
      return records;
    }
  
    /**
     * A function that cleans up a patch object, so that it doesn't introduce unexpected changes when sent to the API.
     * It removes any attributes which are unchanged from the store, to minimise accidental reversions.
     * It also strips out any of links, relationships and meta from `_jv` - See {@link module:jsonapi-vuex~Configuration|Configuration}
     * @memberof module:jsonapi-vuex.utils
     * @param {object} patch - A restructured object to be cleaned
     * @param {object} state={} - Vuex state object (for patch comparison)
     * @param {array} jvProps='[]' - _jv Properties to be kept
     * @return {object} A cleaned copy of the patch object
     */
    cleanPatch(patch, state = {}, jvProps = []) {
      // Add helper properties (use a copy to prevent side-effects)
      const modPatch = this.deepCopy(patch);
      const attrs = get(modPatch, [this.jvtag, 'attrs']);
      const clean = { [this.jvtag]: {} };
      // Only try to clean the patch if it exists in the store
      const stateRecord = get(state, [modPatch[this.jvtag].type, modPatch[this.jvtag].id]);
      if (stateRecord) {
        for (const [k, v] of Object.entries(attrs)) {
          if (!this.hasProperty(stateRecord, k) || !isEqual(stateRecord[k], v)) {
            clean[k] = v;
          }
        }
      } else {
        Object.assign(clean, attrs);
      }
  
      // Add _jv data, as required
      clean[this.jvtag].type = patch[this.jvtag].type;
      clean[this.jvtag].id = patch[this.jvtag].id;
      for (const prop of jvProps) {
        const propVal = get(patch, [this.jvtag, prop]);
        if (propVal) {
          clean[this.jvtag][prop] = propVal;
        }
      }
  
      return clean;
    }
  
    /**
     * Deep copy a normalised object, then re-add helper nethods
     * @memberof module:jsonapi-vuex.utils
     * @param {object} obj - An object to be deep copied
     * @return {object} A deep copied object, with Helper functions added
     */
    deepCopy(obj) {
      let copyObj = this._copy(obj);
      if (Object.entries(copyObj).length) {
        if (this.hasProperty(copyObj, this.jvtag)) {
          copyObj = this.addJvHelpers(copyObj);
        }
        return copyObj;
      }
      return obj;
    }
  
    /**
     * A thin wrapper around `getRelationships, making a copy of the object.
     * We can't add rels to the original object, otherwise Vue's watchers
     * spot the potential for loops (which we are guarding against) and throw an error
     *
     * @memberof module:jsonapi-vuex._internal
     * @param {object} state - Vuex state object
     * @param {object} getters - Vuex getters object
     * @param {object} record - Record to get relationships for.
     * @param {array} seen - internal recursion state-tracking
     * @return {object} records with relationships followed and helper functions added (see {@link module:jsonapi-vuex.utils.addJvHelpers})
     */
    followRelationships(state, getters, record, seen) {
      const data = {};
  
      if (typeof Object.getOwnPropertyDescriptors === 'undefined') {
        Object.getOwnPropertyDescriptors = function(obj) {
          if (obj === null || obj === undefined) {
            throw new TypeError('Cannot convert undefined or null to object');
          }
          const protoPropDescriptor = Object.getOwnPropertyDescriptor(obj, '__proto__');
          const descriptors = protoPropDescriptor ? { ['__proto__']: protoPropDescriptor } : {}
  
          for (const name of Object.getOwnPropertyNames(obj)) {
            descriptors[name] = Object.getOwnPropertyDescriptor(obj, name);
          }
          return descriptors;
        }
      }
  
      Object.defineProperties(data, Object.getOwnPropertyDescriptors(record));
  
      const relationships = this.getRelationships(getters, data, seen);
      Object.defineProperties(data, Object.getOwnPropertyDescriptors(relationships));
  
      return this.addJvHelpers(data);
    }
  
    /**
     * Make a copy of a restructured object, adding (js) getters for its relationships
     * That call the (vuex) get getter to fetch that record from the store
     *
     * Already seen objects are tracked using the 'seen' param to avoid loops.
     *
     * @memberof module:jsonapi-vuex._internal
     * @param {object} getters - Vuex getters object
     * @param {object} parent - The object whose relationships should be fetched
     * @param {array} seen - Internal recursion state tracking
     * @returns {object} A copy of the object with getter relationships added
     */
    getRelationships(getters, parent, seen = []) {
      // Avoid 'this' confusion in Object.defineProperty
      const { conf } = this;
      const { jvtag } = this;
      const relationships = get(parent, [jvtag, 'relationships'], {});
      const relationshipsData = {};
      for (const relName of Object.keys(relationships)) {
        const relations = get(relationships, [relName, 'data']);
        relationshipsData[relName] = {};
        if (relations) {
          const isItem = !Array.isArray(relations);
          const relationsData = [];
  
          for (const relation of isItem ? Array.of(relations) : relations) {
            const relType = relation.type;
            const relId = relation.id;
  
            const current = [relName, relType, relId];
            let data = {};
            // Stop if seen contains an array which matches 'current'
            if (!conf.recurseRelationships && seen.some(a => a.every((v, i) => v === current[i]))) {
              data = { [jvtag]: { type: relType, id: relId } };
            } else {
              // prettier-ignore
              data = getters.get(
                  `${relType}/${relId}`,
                  undefined,
                  [...seen, [relName, relType, relId]]
                )
            }
  
            if (relationsData.indexOf(data) === -1) {
              relationsData.push(data);
            }
          }
          if (isItem) {
            relationshipsData[relName] = relationsData[0];
          } else {
            relationshipsData[relName] = relationsData;
          }
        }
      }
      return relationshipsData;
    }
  
    /**
     * Get the type, id and relationships from a restructured object
     * @memberof module:jsonapi-vuex.utils
     * @param {object} data - A restructured object
     * @return {array} An array (optionally) containing type, id and rels
     */
    getTypeId(data) {
      let type;
      let id;
      let rel;
      if (typeof data === 'string') {
        [type, id, rel] = data.replace(/^\//, '').split('/');
      } else {
        ({ type, id } = data[this.jvtag]);
      }
  
      // Spec: The values of the id and type members MUST be strings.
      // uri encode to prevent mis-interpretation as url parts.
      // Strip any empty strings (falsey items)
      return [
        type && encodeURIComponent(type),
        id && encodeURIComponent(id),
        rel && encodeURIComponent(rel),
      ].filter(Boolean);
    }
  
    /**
     * Return the URL path (links.self) or construct from type/id
     * @memberof module:jsonapi-vuex.utils
     * @param {object} data - A restructured object
     * @return {string} The record's URL path
     */
    getURL(data, post = false) {
      let path = data;
      if (typeof data === 'object') {
        if (get(data, [this.jvtag, 'links', 'self']) && !post) {
          path = data[this.jvtag].links.self;
        } else {
          const { type, id } = data[this.jvtag];
          path = type;
          // POST endpoints are always to collections, not items
          if (id && !post) {
            path += `/${id}`;
          }
        }
      }
      return path;
    }
  
    /**
     * Shorthand for the 'safe' `hasOwnProperty` as described here:
     * [eslint: no-prototype-builtins](https://eslint.org/docs/rules/no-prototype-builtins])
     * @name hasProperty
     * @memberof module:jsonapi-vuex._internal
     */
    hasProperty(obj, prop) {
      return Object.prototype.hasOwnProperty.call(obj, prop);
    }
  
    /**
     * Convert JSONAPI record(s) to restructured data
     * @memberof module:jsonapi-vuex.utils
     * @param {object} data - The `data` object from a JSONAPI record
     * @return {object} Restructured data
     */
    jsonapiToNorm(data) {
      const norm = {};
      if (Array.isArray(data)) {
        data.forEach(item => {
          const { id } = item;
          if (!this.hasProperty(norm, id)) {
            norm[id] = {};
          }
          Object.assign(norm[id], this.jsonapiToNormItem(item));
        });
      } else {
        Object.assign(norm, this.jsonapiToNormItem(data));
      }
      return norm;
    }
  
    /**
     * Restructure a single jsonapi item. Used internally by {@link module:jsonapi-vuex.utils.jsonapiToNorm}
     * @memberof module:jsonapi-vuex._internal
     * @param {object} data - JSONAPI record
     * @return {object} Restructured data
     */
    jsonapiToNormItem(data) {
      if (!data) {
        return {};
      }
      // Move attributes to top-level, nest original jsonapi under _jv
      const norm = { [this.jvtag]: data, ...data.attributes };
      // Create a new object omitting attributes
      const { attributes, ...normNoAttrs } = norm[this.jvtag]; // eslint-disable-line no-unused-vars
      norm[this.jvtag] = normNoAttrs;
      return norm;
    }
  
    /**
     * Convert one or more restructured records to jsonapi
     * @memberof module:jsonapi-vuex.utils
     * @param {object} record - A restructured record to be convert to JSONAPI
     * @return {object} JSONAPI record
     */
    normToJsonapi(record) {
      const jsonapi = [];
      if (!this.hasProperty(record, this.jvtag)) {
        // Collection of id-indexed records
        for (const item of Object.values(record)) {
          jsonapi.push(this.normToJsonapiItem(item));
        }
      } else {
        jsonapi.push(this.normToJsonapiItem(record));
      }
      if (jsonapi.length === 1) {
        return { data: jsonapi[0] };
      }
      return { data: jsonapi };
    }
  
    /**
     * Convert a single restructured item to JSONAPI. Used internally by {@link module:jsonapi-vuex.utils.normToJsonapi}
     * @memberof module:jsonapi-vuex._internal
     * @param {object} data - Restructured data
     * @return {object}  JSONAPI record
     */
    normToJsonapiItem(data) {
      const jsonapi = {};
      // Pick out expected resource members, if they exist
      for (const member of ['id', 'type', 'relationships', 'meta', 'links']) {
        if (this.hasProperty(data[this.jvtag], member)) {
          jsonapi[member] = data[this.jvtag][member];
        }
      }
      // User-generated data (e.g. post) has no helper functions
      if (this.hasProperty(data[this.jvtag], 'attrs')) {
        jsonapi.attributes = data[this.jvtag].attrs;
      } else {
        jsonapi.attributes = { ...data };
        delete jsonapi.attributes[this.jvtag];
      }
      return jsonapi;
    }
  
    /**
     * Convert one or more restructured records to nested (type & id) 'store' object
     * @memberof module:jsonapi-vuex.utils
     * @param {object} record - A restructured record to be convert to JSONAPI
     * @return {object} Structured 'store' object
     */
    normToStore(record) {
      const store = {};
      if (this.hasProperty(record, this.jvtag)) {
        // Convert item to look like a collection
        record = { [record[this.jvtag].id]: record };
      }
      for (const item of Object.values(record)) {
        const { type, id } = item[this.jvtag];
        if (!this.hasProperty(store, type)) {
          store[type] = {};
        }
        if (this.conf.followRelationshipsData) {
          for (const rel in item[this.jvtag].rels) {
            delete item[rel];
          }
        }
        store[type][id] = item;
      }
      return store;
    }
  
    /**
     * If `preserveJSON` is set, add the returned JSONAPI in a get action to _jv.json
     * See {@link module:jsonapi-vuex~Configuration|Configuration}
     * @memberof module:jsonapi-vuex._internal
     * @param {object} data - Restructured record
     * @param {object} json - JSONAPI record
     * @return {object} data record, with JSONAPI added in _jv.json
     */
    preserveJSON(data, json) {
      if (this.conf.preserveJson && data) {
        if (!this.hasProperty(data, this.jvtag)) {
          data[this.jvtag] = {};
        }
        // Store original json in _jv
        data[this.jvtag].json = json;
      }
      return data;
    }
  
    /**
     * Restructure all records in 'included' (using {@link module:jsonapi-vuex._internal.jsonapiToNormItem})
     * and add to the store.
     * @memberof module:jsonapi-vuex._internal
     * @param {object} context - Vuex actions context object
     * @param {object} results - JSONAPI record
     */
    processIncludedRecords(context, results) {
      for (const item of get(results, ['data', 'included'], [])) {
        const includedItem = this.jsonapiToNormItem(item);
        context.commit('addRecords', includedItem);
      }
    }
  
    /**
     * Push resources contained within an API payload into the store.
     *
     * @param {Object} payload
     * @return {Model|Model[]} The model(s) representing the resource(s) contained
     *     within the 'data' key of the payload.
     * @public
     */
    pushPayload(payload, context) {
      this.context = context;
  
      if (payload.included) payload.included.map(this.pushObject.bind(this));
  
      let result =
        payload.data instanceof Array
          ? payload.data.map(this.pushObject.bind(this))
          : this.pushObject(payload.data);
  
      result = this.preserveJSON(result, payload);
  
      return result;
    }
  
    /**
     * Create a model to represent a resource object (or update an existing one),
     * and push it into the store.
     *
     * @param {Object} data The resource object
     * @return {Model|null} The model, or null if no model class has been
     *     registered for this resource type.
     * @public
     */
    pushObject(data) {
      let item = this.jsonapiToNormItem(data);
      // 注册扩展的时候不存store,因为id为空，type一直
      if (data.type === 'user_sign_in' && !data.id) {
      } else {
        this.context.commit('addRecords', item);
      }
      
  
      if (this.conf.followRelationshipsData) {
        item = this.followRelationships(this.context.state, this.context.getters, item);
      }
      return item;
    }
  
    /**
     * Transform args to always be an array (data and config options).
     * See {@link module:jsonapi-vuex.actions} for an explanation of why this function is needed.
     *
     * @memberof module:jsonapi-vuex._internal
     * @param {(string|array)} args - Array of data and configuration info
     * @return {array} Array of data and config options
     */
    unpackArgs(args) {
      if (Array.isArray(args)) {
        return args;
      }
      return [args, {}];
    }
  
    /**
     * A single function to encapsulate the different merge approaches of the record mutations.
     * See {@link module:jsonapi-vuex.mutations} to see the mutations that use this function.
     *
     * @memberof module:jsonapi-vuex._internal
     * @param {object} state - Vuex state object
     * @param {object} records - Restructured records to be updated
     * @param {boolean} merging - Whether or not to merge or overwrite records
     */
    updateRecords(state, records, merging = this.conf.mergeRecords) {
      const storeRecords = this.normToStore(records);
      for (const [type, item] of Object.entries(storeRecords)) {
        if (!this.hasProperty(state, type)) {
          Vue.set(state, type, {});
          // If there's no type, then there are no existing records to merge
          merging = false;
        }
        for (let [id, data] of Object.entries(item)) {
          if (merging) {
            const oldRecord = get(state, [type, id]);
            if (oldRecord) {
              if (
                this.hasProperty(oldRecord, '_jv') &&
                this.hasProperty(oldRecord._jv, 'relationships')
              ) {
                Object.keys(oldRecord._jv.relationships).forEach(item => {
                  if (this.hasProperty(data, '_jv') && this.hasProperty(data._jv, 'relationships')) {
                    if (this.hasProperty(data._jv.relationships, item)) {
                      oldRecord._jv.relationships[item] = data._jv.relationships[item];
                    }
                  }
                });
              }
              if (data._jv.type === 'users') {
                data.avatarUrl = oldRecord.avatarUrl;
              }
              data = merge(oldRecord, data);
            }
          }
          Vue.set(state[type], id, data);
        }
      }
    }
  };
  
  /**
   * A class for tracking the status of actions.
   *
   * **Note** - an instance of this class is exported as `status` from `jsonapi-vuex`,
   * so it does not need to be used directly.
   *
   * @namespace status
   * @memberof module:jsonapi-vuex
   * @param {object} maxId=-1 - Limit ID 'history' to N items (default is unlimited).
   */
  const ActionStatus = class {
    constructor(maxID = -1) {
      this.PENDING = 0;
      this.SUCCESS = 1;
      this.ERROR = -1;
      this.maxID = maxID || -1;
      this.status = {};
      this.counter = 0;
    }
  
    _count() {
      if (this.counter === this.maxID) {
        this.counter = 0;
      }
      return ++this.counter;
    }
  
    /**
     * This method:
     * - Creates a new ID property in the class' `status` object.
     * - Sets status to PENDING
     * - Calls the provided function
     * - Attaches then/catch blocks to the promise which will set status to SUCCESS/ERROR
     *
     * @memberof module:jsonapi-vuex.status
     * @params {function} func - A function to be executed which returns a promised
     * @returns {integer} The status ID for this function.
     */
    run(func) {
      const id = this._count();
      this.status[id] = this.PENDING;
      const promise = new Promise((resolve, reject) => {
        func()
          .then(result => {
            this.status[id] = this.SUCCESS;
            resolve(result);
          })
          .catch(error => {
            this.status[id] = this.ERROR;
            reject(error);
          });
      });
      promise._statusID = id;
      return promise;
    }
  };
  
  export { Utils, ActionStatus };
  