# object.js

    const _ = require('underscore');
    const AVError = require('./error');
    const { _request } = require('./request');
    const {
      isNullOrUndefined,
      ensureArray,
      transformFetchOptions,
      setValue,
      findValue,
      isPlainObject,
    } = require('./utils');

    const recursiveToPointer = value => {
      if (_.isArray(value)) return value.map(recursiveToPointer);
      if (isPlainObject(value)) return _.mapObject(value, recursiveToPointer);
      if (_.isObject(value) && value._toPointer) return value._toPointer();
      return value;
    };

    const RESERVED_KEYS = ['objectId', 'createdAt', 'updatedAt'];
    const checkReservedKey = key => {
      if (RESERVED_KEYS.indexOf(key) !== -1) {
        throw new Error(`key[${key}] is reserved`);
      }
    };

    const handleBatchResults = results => {
      const firstError = _.find(results, result => result instanceof Error);
      if (!firstError) {
        return results;
      }
      const error = new AVError(firstError.code, firstError.message);
      error.results = results;
      throw error;
    };

    // Helper function to get a value from a Backbone object as a property
    // or as a function.
    function getValue(object, prop) {
      if (!(object && object[prop])) {
        return null;
      }
      return _.isFunction(object[prop]) ? object[prop]() : object[prop];
    }

    // AV.Object is analogous to the Java AVObject.
    // It also implements the same interface as a Backbone model.

    module.exports = function(AV) {
      /**
       * Creates a new model with defined attributes. A client id (cid) is
       * automatically generated and assigned for you.
       *
       * <p>You won't normally call this method directly.  It is recommended that
       * you use a subclass of <code>AV.Object</code> instead, created by calling
       * <code>extend</code>.</p>
       *
       * <p>However, if you don't want to use a subclass, or aren't sure which
       * subclass is appropriate, you can use this form:<pre>
       *     var object = new AV.Object("ClassName");
       * </pre>
       * That is basically equivalent to:<pre>
       *     var MyClass = AV.Object.extend("ClassName");
       *     var object = new MyClass();
       * </pre></p>
       *
       * @param {Object} attributes The initial set of data to store in the object.
       * @param {Object} options A set of Backbone-like options for creating the
       *     object.  The only option currently supported is "collection".
       * @see AV.Object.extend
       *
       * @class
       *
       * <p>The fundamental unit of AV data, which implements the Backbone Model
       * interface.</p>
       */
      AV.Object = function(attributes, options) {
        // Allow new AV.Object("ClassName") as a shortcut to _create.
        if (_.isString(attributes)) {
          return AV.Object._create.apply(this, arguments);
        }

        attributes = attributes || {};
        if (options && options.parse) {
          attributes = this.parse(attributes);
          attributes = this._mergeMagicFields(attributes);
        }
        var defaults = getValue(this, 'defaults');
        if (defaults) {
          attributes = _.extend({}, defaults, attributes);
        }
        if (options && options.collection) {
          this.collection = options.collection;
        }

        this._serverData = {}; // The last known data for this object from cloud.
        this._opSetQueue = [{}]; // List of sets of changes to the data.
        this._flags = {};
        this.attributes = {}; // The best estimate of this's current data.

        this._hashedJSON = {}; // Hash of values of containers at last save.
        this._escapedAttributes = {};
        this.cid = _.uniqueId('c');
        this.changed = {};
        this._silent = {};
        this._pending = {};
        this.set(attributes, { silent: true });
        this.changed = {};
        this._silent = {};
        this._pending = {};
        this._hasData = true;
        this._previousAttributes = _.clone(this.attributes);
        this.initialize.apply(this, arguments);
      };

      /**
       * @lends AV.Object.prototype
       * @property {String} id The objectId of the AV Object.
       */

      /**
       * Saves the given list of AV.Object.
       * If any error is encountered, stops and calls the error handler.
       *
       * @example
       * AV.Object.saveAll([object1, object2, ...]).then(function(list) {
       *   // All the objects were saved.
       * }, function(error) {
       *   // An error occurred while saving one of the objects.
       * });
       *
       * @param {Array} list A list of <code>AV.Object</code>.
       */
      AV.Object.saveAll = function(list, options) {
        return AV.Object._deepSaveAsync(list, null, options);
      };

      /**
       * Fetch the given list of AV.Object.
       *
       * @param {AV.Object[]} objects A list of <code>AV.Object</code>
       * @param {AuthOptions} options
       * @return {Promise.<AV.Object[]>} The given list of <code>AV.Object</code>, updated
       */

      AV.Object.fetchAll = (objects, options) =>
        AV.Promise.resolve()
          .then(() =>
            _request(
              'batch',
              null,
              null,
              'POST',
              {
                requests: _.map(objects, object => {
                  if (!object.className)
                    throw new Error('object must have className to fetch');
                  if (!object.id) throw new Error('object must have id to fetch');
                  if (object.dirty())
                    throw new Error('object is modified but not saved');
                  return {
                    method: 'GET',
                    path: `/1.1/classes/${object.className}/${object.id}`,
                  };
                }),
              },
              options
            )
          )
          .then(function(response) {
            const results = _.map(objects, function(object, i) {
              if (response[i].success) {
                const fetchedAttrs = object.parse(response[i].success);
                object._cleanupUnsetKeys(fetchedAttrs);
                object._finishFetch(fetchedAttrs);
                return object;
              }
              if (response[i].success === null) {
                return new AVError(AVError.OBJECT_NOT_FOUND, 'Object not found.');
              }
              return new AVError(response[i].error.code, response[i].error.error);
            });
            return handleBatchResults(results);
          });

      // Attach all inheritable methods to the AV.Object prototype.
      _.extend(
        AV.Object.prototype,
        AV.Events,
        /** @lends AV.Object.prototype */ {
          _fetchWhenSave: false,

          /**
           * Initialize is an empty function by default. Override it with your own
           * initialization logic.
           */
          initialize: function() {},

          /**
           * Set whether to enable fetchWhenSave option when updating object.
           * When set true, SDK would fetch the latest object after saving.
           * Default is false.
           *
           * @deprecated use AV.Object#save with options.fetchWhenSave instead
           * @param {boolean} enable  true to enable fetchWhenSave option.
           */
          fetchWhenSave: function(enable) {
            console.warn(
              'AV.Object#fetchWhenSave is deprecated, use AV.Object#save with options.fetchWhenSave instead.'
            );
            if (!_.isBoolean(enable)) {
              throw new Error('Expect boolean value for fetchWhenSave');
            }
            this._fetchWhenSave = enable;
          },

          /**
           * Returns the object's objectId.
           * @return {String} the objectId.
           */
          getObjectId: function() {
            return this.id;
          },

          /**
           * Returns the object's createdAt attribute.
           * @return {Date}
           */
          getCreatedAt: function() {
            return this.createdAt || this.get('createdAt');
          },

          /**
           * Returns the object's updatedAt attribute.
           * @return {Date}
           */
          getUpdatedAt: function() {
            return this.updatedAt || this.get('updatedAt');
          },

          /**
           * Returns a JSON version of the object.
           * @return {Object}
           */
          toJSON: function(key, holder, seenObjects = []) {
            return this._toFullJSON(seenObjects, false);
          },

          /**
           * Returns a JSON version of the object with meta data.
           * Inverse to {@link AV.parseJSON}
           * @since 2.0.0
           * @return {Object}
           */
          toFullJSON(seenObjects = []) {
            return this._toFullJSON(seenObjects);
          },

          _toFullJSON: function(seenObjects, full = true) {
            var json = _.clone(this.attributes);
            if (_.isArray(seenObjects)) {
              var newSeenObjects = seenObjects.concat(this);
            }
            AV._objectEach(json, function(val, key) {
              json[key] = AV._encode(val, newSeenObjects, undefined, full);
            });
            AV._objectEach(this._operations, function(val, key) {
              json[key] = val;
            });

            if (_.has(this, 'id')) {
              json.objectId = this.id;
            }
            _(['createdAt', 'updatedAt']).each(key => {
              if (_.has(this, key)) {
                const val = this[key];
                json[key] = _.isDate(val) ? val.toJSON() : val;
              }
            });
            if (full) {
              json.__type = 'Object';
              if (_.isArray(seenObjects) && seenObjects.length)
                json.__type = 'Pointer';
              json.className = this.className;
            }
            return json;
          },

          /**
           * Updates _hashedJSON to reflect the current state of this object.
           * Adds any changed hash values to the set of pending changes.
           * @private
           */
          _refreshCache: function() {
            var self = this;
            if (self._refreshingCache) {
              return;
            }
            self._refreshingCache = true;
            AV._objectEach(this.attributes, function(value, key) {
              if (value instanceof AV.Object) {
                value._refreshCache();
              } else if (_.isObject(value)) {
                if (self._resetCacheForKey(key)) {
                  self.set(key, new AV.Op.Set(value), { silent: true });
                }
              }
            });
            delete self._refreshingCache;
          },

          /**
           * Returns true if this object has been modified since its last
           * save/refresh.  If an attribute is specified, it returns true only if that
           * particular attribute has been modified since the last save/refresh.
           * @param {String} attr An attribute name (optional).
           * @return {Boolean}
           */
          dirty: function(attr) {
            this._refreshCache();

            var currentChanges = _.last(this._opSetQueue);

            if (attr) {
              return currentChanges[attr] ? true : false;
            }
            if (!this.id) {
              return true;
            }
            if (_.keys(currentChanges).length > 0) {
              return true;
            }
            return false;
          },

          /**
           * Gets a Pointer referencing this Object.
           * @private
           */
          _toPointer: function() {
            // if (!this.id) {
            //   throw new Error("Can't serialize an unsaved AV.Object");
            // }
            return {
              __type: 'Pointer',
              className: this.className,
              objectId: this.id,
            };
          },

          /**
           * Gets the value of an attribute.
           * @param {String} attr The string name of an attribute.
           */
          get: function(attr) {
            switch (attr) {
              case 'objectId':
                return this.id;
              case 'createdAt':
              case 'updatedAt':
                return this[attr];
              default:
                return this.attributes[attr];
            }
          },

          /**
           * Gets a relation on the given class for the attribute.
           * @param {String} attr The attribute to get the relation for.
           * @return {AV.Relation}
           */
          relation: function(attr) {
            var value = this.get(attr);
            if (value) {
              if (!(value instanceof AV.Relation)) {
                throw new Error('Called relation() on non-relation field ' + attr);
              }
              value._ensureParentAndKey(this, attr);
              return value;
            } else {
              return new AV.Relation(this, attr);
            }
          },

          /**
           * Gets the HTML-escaped value of an attribute.
           */
          escape: function(attr) {
            var html = this._escapedAttributes[attr];
            if (html) {
              return html;
            }
            var val = this.attributes[attr];
            var escaped;
            if (isNullOrUndefined(val)) {
              escaped = '';
            } else {
              escaped = _.escape(val.toString());
            }
            this._escapedAttributes[attr] = escaped;
            return escaped;
          },

          /**
           * Returns <code>true</code> if the attribute contains a value that is not
           * null or undefined.
           * @param {String} attr The string name of the attribute.
           * @return {Boolean}
           */
          has: function(attr) {
            return !isNullOrUndefined(this.attributes[attr]);
          },

          /**
           * Pulls "special" fields like objectId, createdAt, etc. out of attrs
           * and puts them on "this" directly.  Removes them from attrs.
           * @param attrs - A dictionary with the data for this AV.Object.
           * @private
           */
          _mergeMagicFields: function(attrs) {
            // Check for changes of magic fields.
            var model = this;
            var specialFields = ['objectId', 'createdAt', 'updatedAt'];
            AV._arrayEach(specialFields, function(attr) {
              if (attrs[attr]) {
                if (attr === 'objectId') {
                  model.id = attrs[attr];
                } else if (
                  (attr === 'createdAt' || attr === 'updatedAt') &&
                  !_.isDate(attrs[attr])
                ) {
                  model[attr] = AV._parseDate(attrs[attr]);
                } else {
                  model[attr] = attrs[attr];
                }
                delete attrs[attr];
              }
            });
            return attrs;
          },

          /**
           * Returns the json to be sent to the server.
           * @private
           */
          _startSave: function() {
            this._opSetQueue.push({});
          },

          /**
           * Called when a save fails because of an error. Any changes that were part
           * of the save need to be merged with changes made after the save. This
           * might throw an exception is you do conflicting operations. For example,
           * if you do:
           *   object.set("foo", "bar");
           *   object.set("invalid field name", "baz");
           *   object.save();
           *   object.increment("foo");
           * then this will throw when the save fails and the client tries to merge
           * "bar" with the +1.
           * @private
           */
          _cancelSave: function() {
            var self = this;
            var failedChanges = _.first(this._opSetQueue);
            this._opSetQueue = _.rest(this._opSetQueue);
            var nextChanges = _.first(this._opSetQueue);
            AV._objectEach(failedChanges, function(op, key) {
              var op1 = failedChanges[key];
              var op2 = nextChanges[key];
              if (op1 && op2) {
                nextChanges[key] = op2._mergeWithPrevious(op1);
              } else if (op1) {
                nextChanges[key] = op1;
              }
            });
            this._saving = this._saving - 1;
          },

          /**
           * Called when a save completes successfully. This merges the changes that
           * were saved into the known server data, and overrides it with any data
           * sent directly from the server.
           * @private
           */
          _finishSave: function(serverData) {
            // Grab a copy of any object referenced by this object. These instances
            // may have already been fetched, and we don't want to lose their data.
            // Note that doing it like this means we will unify separate copies of the
            // same object, but that's a risk we have to take.
            var fetchedObjects = {};
            AV._traverse(this.attributes, function(object) {
              if (object instanceof AV.Object && object.id && object._hasData) {
                fetchedObjects[object.id] = object;
              }
            });

            var savedChanges = _.first(this._opSetQueue);
            this._opSetQueue = _.rest(this._opSetQueue);
            this._applyOpSet(savedChanges, this._serverData);
            this._mergeMagicFields(serverData);
            var self = this;
            AV._objectEach(serverData, function(value, key) {
              self._serverData[key] = AV._decode(value, key);

              // Look for any objects that might have become unfetched and fix them
              // by replacing their values with the previously observed values.
              var fetched = AV._traverse(self._serverData[key], function(object) {
                if (object instanceof AV.Object && fetchedObjects[object.id]) {
                  return fetchedObjects[object.id];
                }
              });
              if (fetched) {
                self._serverData[key] = fetched;
              }
            });
            this._rebuildAllEstimatedData();
            const opSetQueue = this._opSetQueue.map(_.clone);
            this._refreshCache();
            this._opSetQueue = opSetQueue;
            this._saving = this._saving - 1;
          },

          /**
           * Called when a fetch or login is complete to set the known server data to
           * the given object.
           * @private
           */
          _finishFetch: function(serverData, hasData) {
            // Clear out any changes the user might have made previously.
            this._opSetQueue = [{}];

            // Bring in all the new server data.
            this._mergeMagicFields(serverData);
            var self = this;
            AV._objectEach(serverData, function(value, key) {
              self._serverData[key] = AV._decode(value, key);
            });

            // Refresh the attributes.
            this._rebuildAllEstimatedData();

            // Clear out the cache of mutable containers.
            this._refreshCache();
            this._opSetQueue = [{}];

            this._hasData = hasData;
          },

          /**
           * Applies the set of AV.Op in opSet to the object target.
           * @private
           */
          _applyOpSet: function(opSet, target) {
            var self = this;
            AV._objectEach(opSet, function(change, key) {
              const [value, actualTarget, actualKey] = findValue(target, key);
              setValue(target, key, change._estimate(value, self, key));
              if (actualTarget && actualTarget[actualKey] === AV.Op._UNSET) {
                delete actualTarget[actualKey];
              }
            });
          },

          /**
           * Replaces the cached value for key with the current value.
           * Returns true if the new value is different than the old value.
           * @private
           */
          _resetCacheForKey: function(key) {
            var value = this.attributes[key];
            if (
              _.isObject(value) &&
              !(value instanceof AV.Object) &&
              !(value instanceof AV.File)
            ) {
              var json = JSON.stringify(recursiveToPointer(value));
              if (this._hashedJSON[key] !== json) {
                var wasSet = !!this._hashedJSON[key];
                this._hashedJSON[key] = json;
                return wasSet;
              }
            }
            return false;
          },

          /**
           * Populates attributes[key] by starting with the last known data from the
           * server, and applying all of the local changes that have been made to that
           * key since then.
           * @private
           */
          _rebuildEstimatedDataForKey: function(key) {
            var self = this;
            delete this.attributes[key];
            if (this._serverData[key]) {
              this.attributes[key] = this._serverData[key];
            }
            AV._arrayEach(this._opSetQueue, function(opSet) {
              var op = opSet[key];
              if (op) {
                const [value, actualTarget, actualKey, firstKey] = findValue(
                  self.attributes,
                  key
                );
                setValue(self.attributes, key, op._estimate(value, self, key));
                if (actualTarget && actualTarget[actualKey] === AV.Op._UNSET) {
                  delete actualTarget[actualKey];
                }
                self._resetCacheForKey(firstKey);
              }
            });
          },

          /**
           * Populates attributes by starting with the last known data from the
           * server, and applying all of the local changes that have been made since
           * then.
           * @private
           */
          _rebuildAllEstimatedData: function() {
            var self = this;

            var previousAttributes = _.clone(this.attributes);

            this.attributes = _.clone(this._serverData);
            AV._arrayEach(this._opSetQueue, function(opSet) {
              self._applyOpSet(opSet, self.attributes);
              AV._objectEach(opSet, function(op, key) {
                self._resetCacheForKey(key);
              });
            });

            // Trigger change events for anything that changed because of the fetch.
            AV._objectEach(previousAttributes, function(oldValue, key) {
              if (self.attributes[key] !== oldValue) {
                self.trigger('change:' + key, self, self.attributes[key], {});
              }
            });
            AV._objectEach(this.attributes, function(newValue, key) {
              if (!_.has(previousAttributes, key)) {
                self.trigger('change:' + key, self, newValue, {});
              }
            });
          },

          /**
           * Sets a hash of model attributes on the object, firing
           * <code>"change"</code> unless you choose to silence it.
           *
           * <p>You can call it with an object containing keys and values, or with one
           * key and value.  For example:</p>
           *
           * @example
           * gameTurn.set({
           *   player: player1,
           *   diceRoll: 2
           * }, {
           *   error: function(gameTurnAgain, error) {
           *     // The set failed validation.
           *   }
           * });
           *
           * game.set("currentPlayer", player2, {
           *   error: function(gameTurnAgain, error) {
           *     // The set failed validation.
           *   }
           * });
           *
           * game.set("finished", true);
           *
           * @param {String} key The key to set.
           * @param {Any} value The value to give it.
           * @param {Object} [options]
           * @param {Boolean} [options.silent]
           * @return {AV.Object} self if succeeded, throws if the value is not valid.
           * @see AV.Object#validate
           */
          set: function(key, value, options) {
            var attrs;
            if (_.isObject(key) || isNullOrUndefined(key)) {
              attrs = _.mapObject(key, function(v, k) {
                checkReservedKey(k);
                return AV._decode(v, k);
              });
              options = value;
            } else {
              attrs = {};
              checkReservedKey(key);
              attrs[key] = AV._decode(value, key);
            }

            // Extract attributes and options.
            options = options || {};
            if (!attrs) {
              return this;
            }
            if (attrs instanceof AV.Object) {
              attrs = attrs.attributes;
            }

            // If the unset option is used, every attribute should be a Unset.
            if (options.unset) {
              AV._objectEach(attrs, function(unused_value, key) {
                attrs[key] = new AV.Op.Unset();
              });
            }

            // Apply all the attributes to get the estimated values.
            var dataToValidate = _.clone(attrs);
            var self = this;
            AV._objectEach(dataToValidate, function(value, key) {
              if (value instanceof AV.Op) {
                dataToValidate[key] = value._estimate(
                  self.attributes[key],
                  self,
                  key
                );
                if (dataToValidate[key] === AV.Op._UNSET) {
                  delete dataToValidate[key];
                }
              }
            });

            // Run validation.
            this._validate(attrs, options);

            options.changes = {};
            var escaped = this._escapedAttributes;
            var prev = this._previousAttributes || {};

            // Update attributes.
            AV._arrayEach(_.keys(attrs), function(attr) {
              var val = attrs[attr];

              // If this is a relation object we need to set the parent correctly,
              // since the location where it was parsed does not have access to
              // this object.
              if (val instanceof AV.Relation) {
                val.parent = self;
              }

              if (!(val instanceof AV.Op)) {
                val = new AV.Op.Set(val);
              }

              // See if this change will actually have any effect.
              var isRealChange = true;
              if (
                val instanceof AV.Op.Set &&
                _.isEqual(self.attributes[attr], val.value)
              ) {
                isRealChange = false;
              }

              if (isRealChange) {
                delete escaped[attr];
                if (options.silent) {
                  self._silent[attr] = true;
                } else {
                  options.changes[attr] = true;
                }
              }

              var currentChanges = _.last(self._opSetQueue);
              currentChanges[attr] = val._mergeWithPrevious(currentChanges[attr]);
              self._rebuildEstimatedDataForKey(attr);

              if (isRealChange) {
                self.changed[attr] = self.attributes[attr];
                if (!options.silent) {
                  self._pending[attr] = true;
                }
              } else {
                delete self.changed[attr];
                delete self._pending[attr];
              }
            });

            if (!options.silent) {
              this.change(options);
            }
            return this;
          },

          /**
           * Remove an attribute from the model, firing <code>"change"</code> unless
           * you choose to silence it. This is a noop if the attribute doesn't
           * exist.
           */
          unset: function(attr, options) {
            options = options || {};
            options.unset = true;
            return this.set(attr, null, options);
          },

          /**
           * Atomically increments the value of the given attribute the next time the
           * object is saved. If no amount is specified, 1 is used by default.
           *
           * @param attr {String} The key.
           * @param amount {Number} The amount to increment by.
           */
          increment: function(attr, amount) {
            if (_.isUndefined(amount) || _.isNull(amount)) {
              amount = 1;
            }
            return this.set(attr, new AV.Op.Increment(amount));
          },

          /**
           * Atomically add an object to the end of the array associated with a given
           * key.
           * @param attr {String} The key.
           * @param item {} The item to add.
           */
          add: function(attr, item) {
            return this.set(attr, new AV.Op.Add(ensureArray(item)));
          },

          /**
           * Atomically add an object to the array associated with a given key, only
           * if it is not already present in the array. The position of the insert is
           * not guaranteed.
           *
           * @param attr {String} The key.
           * @param item {} The object to add.
           */
          addUnique: function(attr, item) {
            return this.set(attr, new AV.Op.AddUnique(ensureArray(item)));
          },

          /**
           * Atomically remove all instances of an object from the array associated
           * with a given key.
           *
           * @param attr {String} The key.
           * @param item {} The object to remove.
           */
          remove: function(attr, item) {
            return this.set(attr, new AV.Op.Remove(ensureArray(item)));
          },

          /**
           * Atomically apply a "bit and" operation on the value associated with a
           * given key.
           *
           * @param attr {String} The key.
           * @param value {Number} The value to apply.
           */
          bitAnd(attr, value) {
            return this.set(attr, new AV.Op.BitAnd(value));
          },

          /**
           * Atomically apply a "bit or" operation on the value associated with a
           * given key.
           *
           * @param attr {String} The key.
           * @param value {Number} The value to apply.
           */
          bitOr(attr, value) {
            return this.set(attr, new AV.Op.BitOr(value));
          },

          /**
           * Atomically apply a "bit xor" operation on the value associated with a
           * given key.
           *
           * @param attr {String} The key.
           * @param value {Number} The value to apply.
           */
          bitXor(attr, value) {
            return this.set(attr, new AV.Op.BitXor(value));
          },

          /**
           * Returns an instance of a subclass of AV.Op describing what kind of
           * modification has been performed on this field since the last time it was
           * saved. For example, after calling object.increment("x"), calling
           * object.op("x") would return an instance of AV.Op.Increment.
           *
           * @param attr {String} The key.
           * @returns {AV.Op} The operation, or undefined if none.
           */
          op: function(attr) {
            return _.last(this._opSetQueue)[attr];
          },

          /**
           * Clear all attributes on the model, firing <code>"change"</code> unless
           * you choose to silence it.
           */
          clear: function(options) {
            options = options || {};
            options.unset = true;
            var keysToClear = _.extend(this.attributes, this._operations);
            return this.set(keysToClear, options);
          },

          /**
           * Returns a JSON-encoded set of operations to be sent with the next save
           * request.
           * @private
           */
          _getSaveJSON: function() {
            var json = _.clone(_.first(this._opSetQueue));
            AV._objectEach(json, function(op, key) {
              json[key] = op.toJSON();
            });
            return json;
          },

          /**
           * Returns true if this object can be serialized for saving.
           * @private
           */
          _canBeSerialized: function() {
            return AV.Object._canBeSerializedAsValue(this.attributes);
          },

          /**
           * Fetch the model from the server. If the server's representation of the
           * model differs from its current attributes, they will be overriden,
           * triggering a <code>"change"</code> event.
           * @param {Object} fetchOptions Optional options to set 'keys',
           *      'include' and 'includeACL' option.
           * @param {AuthOptions} options
           * @return {Promise} A promise that is fulfilled when the fetch
           *     completes.
           */
          fetch: function(fetchOptions = {}, options) {
            var self = this;
            var request = _request(
              'classes',
              this.className,
              this.id,
              'GET',
              transformFetchOptions(fetchOptions),
              options
            );
            return request.then(function(response) {
              const fetchedAttrs = self.parse(response);
              if (!fetchOptions.keys) self._cleanupUnsetKeys(fetchedAttrs);
              self._finishFetch(fetchedAttrs, true);
              return self;
            });
          },

          _cleanupUnsetKeys(fetchedAttrs) {
            AV._objectEach(this._serverData, (value, key) => {
              if (fetchedAttrs[key] === undefined) delete this._serverData[key];
            });
          },

          /**
           * Set a hash of model attributes, and save the model to the server.
           * updatedAt will be updated when the request returns.
           * You can either call it as:<pre>
           *   object.save();</pre>
           * or<pre>
           *   object.save(null, options);</pre>
           * or<pre>
           *   object.save(attrs, options);</pre>
           * or<pre>
           *   object.save(key, value, options);</pre>
           *
           * @example
           * gameTurn.save({
           *   player: "Jake Cutter",
           *   diceRoll: 2
           * }).then(function(gameTurnAgain) {
           *   // The save was successful.
           * }, function(error) {
           *   // The save failed.  Error is an instance of AVError.
           * });
           *
           * @param {AuthOptions} options AuthOptions plus:
           * @param {Boolean} options.fetchWhenSave fetch and update object after save succeeded
           * @param {AV.Query} options.query Save object only when it matches the query
           * @return {AV.Promise} A promise that is fulfilled when the save
           *     completes.
           * @see AVError
           */
          save: function(arg1, arg2, arg3) {
            var i, attrs, current, options, saved;
            if (_.isObject(arg1) || isNullOrUndefined(arg1)) {
              attrs = arg1;
              options = arg2;
            } else {
              attrs = {};
              attrs[arg1] = arg2;
              options = arg3;
            }

            options = _.clone(options) || {};
            if (options.wait) {
              current = _.clone(this.attributes);
            }

            var setOptions = _.clone(options) || {};
            if (setOptions.wait) {
              setOptions.silent = true;
            }
            if (attrs) {
              this.set(attrs, setOptions);
            }

            var model = this;

            var unsavedChildren = [];
            var unsavedFiles = [];
            AV.Object._findUnsavedChildren(model, unsavedChildren, unsavedFiles);
            if (unsavedChildren.length + unsavedFiles.length > 1) {
              return AV.Object._deepSaveAsync(this, model, options);
            }

            this._startSave();
            this._saving = (this._saving || 0) + 1;

            this._allPreviousSaves = this._allPreviousSaves || AV.Promise.resolve();
            this._allPreviousSaves = this._allPreviousSaves
              .catch(e => {})
              .then(function() {
                var method = model.id ? 'PUT' : 'POST';

                var json = model._getSaveJSON();
                var query = {};

                if (model._fetchWhenSave || options.fetchWhenSave) {
                  query['new'] = 'true';
                }

                if (options.query) {
                  var queryJSON;
                  if (typeof options.query.toJSON === 'function') {
                    queryJSON = options.query.toJSON();
                    if (queryJSON) {
                      query.where = queryJSON.where;
                    }
                  }
                  if (!query.where) {
                    var error = new Error('options.query is not an AV.Query');
                    throw error;
                  }
                }

                _.extend(json, model._flags);

                var route = 'classes';
                var className = model.className;
                if (model.className === '_User' && !model.id) {
                  // Special-case user sign-up.
                  route = 'users';
                  className = null;
                }
                //hook makeRequest in options.
                var makeRequest = options._makeRequest || _request;
                var requestPromise = makeRequest(
                  route,
                  className,
                  model.id,
                  method,
                  json,
                  options,
                  query
                );

                requestPromise = requestPromise.then(
                  function(resp) {
                    var serverAttrs = model.parse(resp);
                    if (options.wait) {
                      serverAttrs = _.extend(attrs || {}, serverAttrs);
                    }
                    model._finishSave(serverAttrs);
                    if (options.wait) {
                      model.set(current, setOptions);
                    }
                    return model;
                  },
                  function(error) {
                    model._cancelSave();
                    throw error;
                  }
                );

                return requestPromise;
              });
            return this._allPreviousSaves;
          },

          /**
           * Destroy this model on the server if it was already persisted.
           * Optimistically removes the model from its collection, if it has one.
           * @param {AuthOptions} options AuthOptions plus:
           * @param {Boolean} [options.wait] wait for the server to respond
           * before removal.
           *
           * @return {Promise} A promise that is fulfilled when the destroy
           *     completes.
           */
          destroy: function(options) {
            options = options || {};
            var model = this;

            var triggerDestroy = function() {
              model.trigger('destroy', model, model.collection, options);
            };

            if (!this.id) {
              return triggerDestroy();
            }

            if (!options.wait) {
              triggerDestroy();
            }

            var request = _request(
              'classes',
              this.className,
              this.id,
              'DELETE',
              this._flags,
              options
            );
            return request.then(function() {
              if (options.wait) {
                triggerDestroy();
              }
              return model;
            });
          },

          /**
           * Converts a response into the hash of attributes to be set on the model.
           * @ignore
           */
          parse: function(resp) {
            var output = _.clone(resp);
            _(['createdAt', 'updatedAt']).each(function(key) {
              if (output[key]) {
                output[key] = AV._parseDate(output[key]);
              }
            });
            if (output.createdAt && !output.updatedAt) {
              output.updatedAt = output.createdAt;
            }
            return output;
          },

          /**
           * Creates a new model with identical attributes to this one.
           * @return {AV.Object}
           */
          clone: function() {
            return new this.constructor(this.attributes);
          },

          /**
           * Returns true if this object has never been saved to AV.
           * @return {Boolean}
           */
          isNew: function() {
            return !this.id;
          },

          /**
           * Call this method to manually fire a `"change"` event for this model and
           * a `"change:attribute"` event for each changed attribute.
           * Calling this will cause all objects observing the model to update.
           */
          change: function(options) {
            options = options || {};
            var changing = this._changing;
            this._changing = true;

            // Silent changes become pending changes.
            var self = this;
            AV._objectEach(this._silent, function(attr) {
              self._pending[attr] = true;
            });

            // Silent changes are triggered.
            var changes = _.extend({}, options.changes, this._silent);
            this._silent = {};
            AV._objectEach(changes, function(unused_value, attr) {
              self.trigger('change:' + attr, self, self.get(attr), options);
            });
            if (changing) {
              return this;
            }

            // This is to get around lint not letting us make a function in a loop.
            var deleteChanged = function(value, attr) {
              if (!self._pending[attr] && !self._silent[attr]) {
                delete self.changed[attr];
              }
            };

            // Continue firing `"change"` events while there are pending changes.
            while (!_.isEmpty(this._pending)) {
              this._pending = {};
              this.trigger('change', this, options);
              // Pending and silent changes still remain.
              AV._objectEach(this.changed, deleteChanged);
              self._previousAttributes = _.clone(this.attributes);
            }

            this._changing = false;
            return this;
          },

          /**
           * Determine if the model has changed since the last <code>"change"</code>
           * event.  If you specify an attribute name, determine if that attribute
           * has changed.
           * @param {String} attr Optional attribute name
           * @return {Boolean}
           */
          hasChanged: function(attr) {
            if (!arguments.length) {
              return !_.isEmpty(this.changed);
            }
            return this.changed && _.has(this.changed, attr);
          },

          /**
           * Returns an object containing all the attributes that have changed, or
           * false if there are no changed attributes. Useful for determining what
           * parts of a view need to be updated and/or what attributes need to be
           * persisted to the server. Unset attributes will be set to undefined.
           * You can also pass an attributes object to diff against the model,
           * determining if there *would be* a change.
           */
          changedAttributes: function(diff) {
            if (!diff) {
              return this.hasChanged() ? _.clone(this.changed) : false;
            }
            var changed = {};
            var old = this._previousAttributes;
            AV._objectEach(diff, function(diffVal, attr) {
              if (!_.isEqual(old[attr], diffVal)) {
                changed[attr] = diffVal;
              }
            });
            return changed;
          },

          /**
           * Gets the previous value of an attribute, recorded at the time the last
           * <code>"change"</code> event was fired.
           * @param {String} attr Name of the attribute to get.
           */
          previous: function(attr) {
            if (!arguments.length || !this._previousAttributes) {
              return null;
            }
            return this._previousAttributes[attr];
          },

          /**
           * Gets all of the attributes of the model at the time of the previous
           * <code>"change"</code> event.
           * @return {Object}
           */
          previousAttributes: function() {
            return _.clone(this._previousAttributes);
          },

          /**
           * Checks if the model is currently in a valid state. It's only possible to
           * get into an *invalid* state if you're using silent changes.
           * @return {Boolean}
           */
          isValid: function() {
            try {
              this.validate(this.attributes);
            } catch (error) {
              return false;
            }
            return true;
          },

          /**
           * You should not call this function directly unless you subclass
           * <code>AV.Object</code>, in which case you can override this method
           * to provide additional validation on <code>set</code> and
           * <code>save</code>.  Your implementation should throw an Error if
           * the attrs is invalid
           *
           * @param {Object} attrs The current data to validate.
           * @see AV.Object#set
           */
          validate: function(attrs) {
            if (_.has(attrs, 'ACL') && !(attrs.ACL instanceof AV.ACL)) {
              throw new AVError(AVError.OTHER_CAUSE, 'ACL must be a AV.ACL.');
            }
          },

          /**
           * Run validation against a set of incoming attributes, returning `true`
           * if all is well. If a specific `error` callback has been passed,
           * call that instead of firing the general `"error"` event.
           * @private
           */
          _validate: function(attrs, options) {
            if (options.silent || !this.validate) {
              return;
            }
            attrs = _.extend({}, this.attributes, attrs);
            this.validate(attrs);
          },

          /**
           * Returns the ACL for this object.
           * @returns {AV.ACL} An instance of AV.ACL.
           * @see AV.Object#get
           */
          getACL: function() {
            return this.get('ACL');
          },

          /**
           * Sets the ACL to be used for this object.
           * @param {AV.ACL} acl An instance of AV.ACL.
           * @param {Object} options Optional Backbone-like options object to be
           *     passed in to set.
           * @return {AV.Object} self
           * @see AV.Object#set
           */
          setACL: function(acl, options) {
            return this.set('ACL', acl, options);
          },

          disableBeforeHook: function() {
            this.ignoreHook('beforeSave');
            this.ignoreHook('beforeUpdate');
            this.ignoreHook('beforeDelete');
          },

          disableAfterHook: function() {
            this.ignoreHook('afterSave');
            this.ignoreHook('afterUpdate');
            this.ignoreHook('afterDelete');
          },

          ignoreHook: function(hookName) {
            if (
              !_.contains(
                [
                  'beforeSave',
                  'afterSave',
                  'beforeUpdate',
                  'afterUpdate',
                  'beforeDelete',
                  'afterDelete',
                ],
                hookName
              )
            ) {
              console.trace('Unsupported hookName: ' + hookName);
            }

            if (!AV.hookKey) {
              console.trace('ignoreHook required hookKey');
            }

            if (!this._flags.__ignore_hooks) {
              this._flags.__ignore_hooks = [];
            }

            this._flags.__ignore_hooks.push(hookName);
          },
        }
      );

      /**
       * Creates an instance of a subclass of AV.Object for the give classname
       * and id.
       * @param  {String} className The name of the AV class backing this model.
       * @param {String} id The object id of this model.
       * @return {AV.Object} A new subclass instance of AV.Object.
       */
      AV.Object.createWithoutData = function(className, id, hasData) {
        var result = new AV.Object(className);
        result.id = id;
        result._hasData = hasData;
        return result;
      };
      /**
       * Delete objects in batch.
       * @param {AV.Object[]} objects The <code>AV.Object</code> array to be deleted.
       * @param {AuthOptions} options
       * @return {Promise} A promise that is fulfilled when the save
       *     completes.
       */
      AV.Object.destroyAll = function(objects, options = {}) {
        if (!objects || objects.length === 0) {
          return AV.Promise.resolve();
        }
        const objectsByClassNameAndFlags = _.groupBy(objects, object =>
          JSON.stringify({
            className: object.className,
            flags: object._flags,
          })
        );
        const body = {
          requests: _.map(objectsByClassNameAndFlags, objects => {
            const ids = _.map(objects, 'id').join(',');
            return {
              method: 'DELETE',
              path: `/1.1/classes/${objects[0].className}/${ids}`,
              body: objects[0]._flags,
            };
          }),
        };
        return _request('batch', null, null, 'POST', body, options).then(
          response => {
            const firstError = _.find(response, result => !result.success);
            if (firstError)
              throw new AVError(firstError.error.code, firstError.error.error);
            return undefined;
          }
        );
      };

      /**
       * Returns the appropriate subclass for making new instances of the given
       * className string.
       * @private
       */
      AV.Object._getSubclass = function(className) {
        if (!_.isString(className)) {
          throw new Error('AV.Object._getSubclass requires a string argument.');
        }
        var ObjectClass = AV.Object._classMap[className];
        if (!ObjectClass) {
          ObjectClass = AV.Object.extend(className);
          AV.Object._classMap[className] = ObjectClass;
        }
        return ObjectClass;
      };

      /**
       * Creates an instance of a subclass of AV.Object for the given classname.
       * @private
       */
      AV.Object._create = function(className, attributes, options) {
        var ObjectClass = AV.Object._getSubclass(className);
        return new ObjectClass(attributes, options);
      };

      // Set up a map of className to class so that we can create new instances of
      // AV Objects from JSON automatically.
      AV.Object._classMap = {};

      AV.Object._extend = AV._extend;

      /**
       * Creates a new model with defined attributes,
       * It's the same with
       * <pre>
       *   new AV.Object(attributes, options);
       *  </pre>
       * @param {Object} attributes The initial set of data to store in the object.
       * @param {Object} options A set of Backbone-like options for creating the
       *     object.  The only option currently supported is "collection".
       * @return {AV.Object}
       * @since v0.4.4
       * @see AV.Object
       * @see AV.Object.extend
       */
      AV.Object['new'] = function(attributes, options) {
        return new AV.Object(attributes, options);
      };

      /**
       * Creates a new subclass of AV.Object for the given AV class name.
       *
       * <p>Every extension of a AV class will inherit from the most recent
       * previous extension of that class. When a AV.Object is automatically
       * created by parsing JSON, it will use the most recent extension of that
       * class.</p>
       *
       * @example
       * var MyClass = AV.Object.extend("MyClass", {
       *     // Instance properties
       * }, {
       *     // Class properties
       * });
       *
       * @param {String} className The name of the AV class backing this model.
       * @param {Object} protoProps Instance properties to add to instances of the
       *     class returned from this method.
       * @param {Object} classProps Class properties to add the class returned from
       *     this method.
       * @return {Class} A new subclass of AV.Object.
       */
      AV.Object.extend = function(className, protoProps, classProps) {
        // Handle the case with only two args.
        if (!_.isString(className)) {
          if (className && _.has(className, 'className')) {
            return AV.Object.extend(className.className, className, protoProps);
          } else {
            throw new Error(
              "AV.Object.extend's first argument should be the className."
            );
          }
        }

        // If someone tries to subclass "User", coerce it to the right type.
        if (className === 'User') {
          className = '_User';
        }

        var NewClassObject = null;
        if (_.has(AV.Object._classMap, className)) {
          var OldClassObject = AV.Object._classMap[className];
          // This new subclass has been told to extend both from "this" and from
          // OldClassObject. This is multiple inheritance, which isn't supported.
          // For now, let's just pick one.
          if (protoProps || classProps) {
            NewClassObject = OldClassObject._extend(protoProps, classProps);
          } else {
            return OldClassObject;
          }
        } else {
          protoProps = protoProps || {};
          protoProps._className = className;
          NewClassObject = this._extend(protoProps, classProps);
        }
        // Extending a subclass should reuse the classname automatically.
        NewClassObject.extend = function(arg0) {
          if (_.isString(arg0) || (arg0 && _.has(arg0, 'className'))) {
            return AV.Object.extend.apply(NewClassObject, arguments);
          }
          var newArguments = [className].concat(_.toArray(arguments));
          return AV.Object.extend.apply(NewClassObject, newArguments);
        };
        // Add the query property descriptor.
        Object.defineProperty(
          NewClassObject,
          'query',
          Object.getOwnPropertyDescriptor(AV.Object, 'query')
        );
        NewClassObject['new'] = function(attributes, options) {
          return new NewClassObject(attributes, options);
        };
        AV.Object._classMap[className] = NewClassObject;
        return NewClassObject;
      };

      // ES6 class syntax support
      Object.defineProperty(AV.Object.prototype, 'className', {
        get: function() {
          const className =
            this._className ||
            this.constructor._LCClassName ||
            this.constructor.name;
          // If someone tries to subclass "User", coerce it to the right type.
          if (className === 'User') {
            return '_User';
          }
          return className;
        },
      });

      /**
       * Register a class.
       * If a subclass of <code>AV.Object</code> is defined with your own implement
       * rather then <code>AV.Object.extend</code>, the subclass must be registered.
       * @param {Function} klass A subclass of <code>AV.Object</code>
       * @param {String} [name] Specify the name of the class. Useful when the class might be uglified.
       * @example
       * class Person extend AV.Object {}
       * AV.Object.register(Person);
       */
      AV.Object.register = (klass, name) => {
        if (!(klass.prototype instanceof AV.Object)) {
          throw new Error('registered class is not a subclass of AV.Object');
        }
        const className = name || klass.name;
        if (!className.length) {
          throw new Error('registered class must be named');
        }
        if (name) {
          klass._LCClassName = name;
        }
        AV.Object._classMap[className] = klass;
      };

      /**
       * Get a new Query of the current class
       * @name query
       * @memberof AV.Object
       * @type AV.Query
       * @readonly
       * @since v3.1.0
       * @example
       * const Post = AV.Object.extend('Post');
       * Post.query.equalTo('author', 'leancloud').find().then();
       */
      Object.defineProperty(AV.Object, 'query', {
        get() {
          return new AV.Query(this.prototype.className);
        },
      });

      AV.Object._findUnsavedChildren = function(objects, children, files) {
        AV._traverse(objects, function(object) {
          if (object instanceof AV.Object) {
            if (object.dirty()) {
              children.push(object);
            }
            return;
          }

          if (object instanceof AV.File) {
            if (!object.url() && !object.id) {
              files.push(object);
            }
            return;
          }
        });
      };

      AV.Object._canBeSerializedAsValue = function(object) {
        var canBeSerializedAsValue = true;

        if (object instanceof AV.Object || object instanceof AV.File) {
          canBeSerializedAsValue = !!object.id;
        } else if (_.isArray(object)) {
          AV._arrayEach(object, function(child) {
            if (!AV.Object._canBeSerializedAsValue(child)) {
              canBeSerializedAsValue = false;
            }
          });
        } else if (_.isObject(object)) {
          AV._objectEach(object, function(child) {
            if (!AV.Object._canBeSerializedAsValue(child)) {
              canBeSerializedAsValue = false;
            }
          });
        }

        return canBeSerializedAsValue;
      };

      AV.Object._deepSaveAsync = function(object, model, options) {
        var unsavedChildren = [];
        var unsavedFiles = [];
        AV.Object._findUnsavedChildren(object, unsavedChildren, unsavedFiles);

        var promise = AV.Promise.resolve();
        _.each(unsavedFiles, function(file) {
          promise = promise.then(function() {
            return file.save();
          });
        });

        var objects = _.uniq(unsavedChildren);
        var remaining = _.uniq(objects);

        return promise
          .then(function() {
            return AV.Promise._continueWhile(
              function() {
                return remaining.length > 0;
              },
              function() {
                // Gather up all the objects that can be saved in this batch.
                var batch = [];
                var newRemaining = [];
                AV._arrayEach(remaining, function(object) {
                  // Limit batches to 20 objects.
                  if (batch.length > 20) {
                    newRemaining.push(object);
                    return;
                  }

                  if (object._canBeSerialized()) {
                    batch.push(object);
                  } else {
                    newRemaining.push(object);
                  }
                });
                remaining = newRemaining;

                // If we can't save any objects, there must be a circular reference.
                if (batch.length === 0) {
                  return AV.Promise.reject(
                    new AVError(
                      AVError.OTHER_CAUSE,
                      'Tried to save a batch with a cycle.'
                    )
                  );
                }

                // Reserve a spot in every object's save queue.
                var readyToStart = AV.Promise.resolve(
                  _.map(batch, function(object) {
                    return object._allPreviousSaves || AV.Promise.resolve();
                  })
                );

                // Save a single batch, whether previous saves succeeded or failed.
                const bathSavePromise = readyToStart.then(() =>
                  _request(
                    'batch',
                    null,
                    null,
                    'POST',
                    {
                      requests: _.map(batch, function(object) {
                        var method = object.id ? 'PUT' : 'POST';

                        var json = object._getSaveJSON();
                        var query = {};

                        _.extend(json, object._flags);

                        var route = 'classes';
                        var className = object.className;
                        var path = `/${route}/${className}`;
                        if (object.className === '_User' && !object.id) {
                          // Special-case user sign-up.
                          path = '/users';
                        }

                        var path = `/1.1${path}`;
                        if (object.id) {
                          path = path + '/' + object.id;
                        }

                        object._startSave();

                        return {
                          method: method,
                          path: path,
                          body: json,
                        };
                      }),
                    },
                    options
                  ).then(function(response) {
                    const results = _.map(batch, function(object, i) {
                      if (response[i].success) {
                        object._finishSave(object.parse(response[i].success));
                        return object;
                      }
                      object._cancelSave();
                      return new AVError(
                        response[i].error.code,
                        response[i].error.error
                      );
                    });
                    return handleBatchResults(results);
                  })
                );
                AV._arrayEach(batch, function(object) {
                  object._allPreviousSaves = bathSavePromise;
                });
                return bathSavePromise;
              }
            );
          })
          .then(function() {
            return object;
          });
      };
    };