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

const recursiveToPointer = value => {
  if (_.isObject(value)) {
    if (value._toPointer) return value._toPointer();
    return _.mapObject(value, recursiveToPointer);
  }
  if (_.isArray(value)) return value.map(recursiveToPointer);
  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();
        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;
      },
<commit_msg>fix(object): _refreshCache in _finishSave<commit_after>const _ = require('underscore');
const AVError = require('./error');
const { _request } = require('./request');
const {
  isNullOrUndefined,
  ensureArray,
  transformFetchOptions,
  setValue,
  findValue,
} = require('./utils');

const recursiveToPointer = value => {
  if (_.isArray(value)) return value.map(recursiveToPointer);
  if (_.isObject(value)) {
    if (value._toPointer) return value._toPointer();
    return _.mapObject(value, recursiveToPointer);
  }
  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;
      },

{'fraction_non_alphanumeric': 0.1193503027520303, 'fraction_numerical': 0.0011478749964128907, 'mean_word_length': 1.9620059498512536, 'pattern_counts': {'":': 0, '<': 49, '<?xml version=': 0, '>': 67, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4758418', 'n_tokens_mistral': 9754, 'n_tokens_neox': 9613, 'n_words': 2829}