/**
 * A Model or Entity represents some object that your application manages. For example, one
 * might define a Model for Users, Products, Cars, or other real-world object that we want
 * to model in the system. Models are used by {@link Ext.data.Store stores}, which are in
 * turn used by many of the data-bound components in Ext.
 *
 * # Fields
 *
 * Models are defined as a set of fields and any arbitrary methods and properties relevant
 * to the model. For example:
 *
 *     Ext.define('User', {
 *         extend: 'Ext.data.Model',
 *         fields: [
 *             {name: 'name',  type: 'string'},
 *             {name: 'age',   type: 'int', convert: null},
 *             {name: 'phone', type: 'string'},
 *             {name: 'alive', type: 'boolean', defaultValue: true, convert: null}
 *         ],
 *
 *         changeName: function() {
 *             var oldName = this.get('name'),
 *                 newName = oldName + " The Barbarian";
 *
 *             this.set('name', newName);
 *         }
 *     });
 *
 * Now we can create instances of our User model and call any model logic we defined:
 *
 *     var user = Ext.create('User', {
 *         id   : 'ABCD12345',
 *         name : 'Conan',
 *         age  : 24,
 *         phone: '555-555-5555'
 *     });
 *
 *     user.changeName();
 *     user.get('name'); //returns "Conan The Barbarian"
 *
 * By default, the built in field types such as number and boolean coerce string values
 * in the raw data by virtue of their {@link Ext.data.field.Field#method-convert} method.
 * When the server can be relied upon to send data in a format that does not need to be
 * converted, disabling this can improve performance. The {@link Ext.data.reader.Json Json}
 * and {@link Ext.data.reader.Array Array} readers are likely candidates for this
 * optimization. To disable field conversions you simply specify `null` for the field's
 * {@link Ext.data.field.Field#cfg-convert convert config}.
 *
 * ## The "id" Field and `idProperty`
 *
 * A Model definition always has an *identifying field* which should yield a unique key
 * for each instance. By default, a field named "id" will be created with a
 * {@link Ext.data.Field#mapping mapping} of "id". This happens because of the default
 * {@link #idProperty} provided in Model definitions.
 *
 * To alter which field is the identifying field, use the {@link #idProperty} config.
 *
 * # Validators
 *
 * Models have built-in support for field validators. Validators are added to models as in
 * the follow example:
 *
 *     Ext.define('User', {
 *         extend: 'Ext.data.Model',
 *         fields: [
 *             { name: 'name',     type: 'string' },
 *             { name: 'age',      type: 'int' },
 *             { name: 'phone',    type: 'string' },
 *             { name: 'gender',   type: 'string' },
 *             { name: 'username', type: 'string' },
 *             { name: 'alive',    type: 'boolean', defaultValue: true }
 *         ],
 *
 *         validators: {
 *             age: 'presence',
 *             name: { type: 'length', min: 2 },
 *             gender: { type: 'inclusion', list: ['Male', 'Female'] },
 *             username: [
 *                 { type: 'exclusion', list: ['Admin', 'Operator'] },
 *                 { type: 'format', matcher: /([a-z]+)[0-9]{2,3}/i }
 *             ]
 *         }
 *     });
 *
 * The derived type of `Ext.data.field.Field` can also provide validation. If `validators`
 * need to be duplicated on multiple fields, instead consider creating a custom field type.
 *
 * ## Validation
 *
 * The results of the validators can be retrieved via the "associated" validation record:
 *
 *     var instance = Ext.create('User', {
 *         name: 'Ed',
 *         gender: 'Male',
 *         username: 'edspencer'
 *     });
 *
 *     var validation = instance.getValidation();
 *
 * The returned object is an instance of `Ext.data.Validation` and has as its fields the
 * result of the field `validators`. The validation object is "dirty" if there are one or
 * more validation errors present.
 *
 * This record is also available when using data binding as a "pseudo-association" called
 * "validation". This pseudo-association can be hidden by an explicitly declared
 * association by the same name (for compatibility reasons), but doing so is not
 * recommended.
 *
 * The `{@link Ext.Component#modelValidation}` config can be used to enable automatic
 * binding from the "validation" of a record to the form fields that may be bound to its
 * values.
 *
 * # Associations
 * 
 * Models often have associations with other Models. These associations can be defined by
 * fields (often called "foreign keys") or by other data such as a many-to-many (or "matrix").
 * See {@link Ext.data.schema.Association} for information about configuring and using associations.
 *
 * # Using a Proxy
 *
 * Models are great for representing types of data and relationships, but sooner or later we're
 * going to want to load or save that data somewhere. All loading and saving of data is handled
 * via a {@link Ext.data.proxy.Proxy Proxy}, which can be set directly on the Model:
 *
 *     Ext.define('User', {
 *         extend: 'Ext.data.Model',
 *         fields: ['id', 'name', 'email'],
 *
 *         proxy: {
 *             type: 'rest',
 *             url : '/users'
 *         }
 *     });
 *
 * Here we've set up a {@link Ext.data.proxy.Rest Rest Proxy}, which knows how to load and save
 * data to and from a RESTful backend. Let's see how this works:
 *
 *     var user = Ext.create('User', {name: 'Ed Spencer', email: 'ed@sencha.com'});
 *
 *     user.save(); //POST /users
 *
 * Calling {@link #save} on the new Model instance tells the configured RestProxy that we wish to
 * persist this Model's data onto our server. RestProxy figures out that this Model hasn't been
 * saved before because it doesn't have an id, and performs the appropriate action - in this case
 * issuing a POST request to the url we configured (/users). We configure any Proxy on any Model
 * and always follow this API - see {@link Ext.data.proxy.Proxy} for a full list.
 *
 * Loading data via the Proxy is accomplished with the static `load` method:
 *
 *     // Uses the configured RestProxy to make a GET request to /users/123
 *     User.load(123, {
 *         success: function(user) {
 *             console.log(user.getId()); //logs 123
 *         }
 *     });
 *
 * Models can also be updated and destroyed easily:
 *
 *     // the user Model we loaded in the last snippet:
 *     user.set('name', 'Edward Spencer');
 *
 *     // tells the Proxy to save the Model. In this case it will perform a PUT request
 *     // to /users/123 as this Model already has an id
 *     user.save({
 *         success: function() {
 *             console.log('The User was updated');
 *         }
 *     });
 *
 *     // tells the Proxy to destroy the Model. Performs a DELETE request to /users/123
 *     user.erase({
 *         success: function() {
 *             console.log('The User was destroyed!');
 *         }
 *     });
 * 
 * # HTTP Parameter names when using a {@link Ext.data.proxy.Ajax Ajax proxy}
 *
 * By default, the model ID is specified in an HTTP parameter named `id`. To change the
 * name of this parameter use the Proxy's {@link Ext.data.proxy.Ajax#idParam idParam}
 * configuration.
 *
 * Parameters for other commonly passed values such as
 * {@link Ext.data.proxy.Ajax#pageParam page number} or
 * {@link Ext.data.proxy.Ajax#startParam start row} may also be configured.
 *
 * # Usage in Stores
 *
 * It is very common to want to load a set of Model instances to be displayed and manipulated
 * in the UI. We do this by creating a {@link Ext.data.Store Store}:
 *
 *     var store = Ext.create('Ext.data.Store', {
 *         model: 'User'
 *     });
 *
 *     //uses the Proxy we set up on Model to load the Store data
 *     store.load();
 *
 * A Store is just a collection of Model instances - usually loaded from a server somewhere. Store
 * can also maintain a set of added, updated and removed Model instances to be synchronized with
 * the server via the Proxy. See the {@link Ext.data.Store Store docs} for more information
 * on Stores.
 */
Ext.define('Ext.data.Model', {
    alternateClassName: 'Ext.data.Record',
 
    requires: [
        'Ext.data.ErrorCollection',
        'Ext.data.operation.*',
        'Ext.data.field.*',
        'Ext.data.validator.Validator',
        'Ext.data.schema.Schema',
        'Ext.data.identifier.Generator',
        'Ext.data.identifier.Sequential'
    ],
    uses: [
        'Ext.data.Validation'
    ],
 
    /**
     * @property {Boolean} isEntity
     * The value `true` to identify this class and its subclasses.
     * @readonly
     */
    isEntity: true,
 
    /**
     * @property {Boolean} isModel
     * The value `true` to identify this class and its subclasses.
     * @readonly
     */
    isModel: true,
 
    // Record ids are more flexible.
    validIdRe: null,
 
    erasing: false,
 
    loadOperation: null,
    loadCount: 0,
 
    observableType: 'record',
 
    /**
     * @property {"C"/"R"/"U"/"D"} crudState
     * This value is initially "R" or "C" indicating the initial CRUD state. As the
     * record changes and the various joined parties (stores, sessions, etc.) are notified
     * this value is updated prior to these calls. In other words, the joined parties
     * are notified after the `crudState` is updated. This means that the `crudState`
     * property may be briefly out of sync with underlying changes if this state is used
     * outside of these notifications.
     *
     * The possible states have these meanings:
     *
     *  * "R" - The record is in a cleanly retrieved (unmodified) state.
     *  * "C" - The record is in a newly created (`phantom`) state.
     *  * "U" - The record is in an updated, `modified` (`dirty`) state.
     *  * "D" - The record is in a `dropped` state.
     *
     * @readonly
     * @protected
     * @since 6.2.0
     */
    crudState: 'R',
 
    /**
     * @property {"C"/"R"/"U"/"D"} crudStateWas
     * This value is initially `null` indicating there is no previous CRUD state. As the
     * record changes and the various joined parties (stores, sessions, etc.) are notified
     * this value is updated for the *subsequent* calls. In other words, the joined parties
     * are notified and then `crudStateWas` is modified for the next update.
     *
     * The value of this property has the same meaning as `crudState`.
     *
     * @readonly
     * @protected
     * @since 6.2.0
     */
    crudStateWas: null,
 
    constructor: function(data, session, skipStoreAddition) {
        var me = this,
            cls = me.self,
            identifier = cls.identifier,
            Model = Ext.data.Model,
            modelIdentifier = Model.identifier,
            idProperty = me.idField.name,
            array, id, initializeFn, internalId, len, i, fields;
 
        // Yes, this is here on purpose. See EXTJS-16494. The second
        // assignment seems to work around a strange JIT issue that prevents
        // this.data being assigned in random scenarios, even though the data
        // is passed into the constructor. The issue occurs on 4th gen iPads and
        // lower, possibly other older iOS devices.
        // A similar issue can occur with the hasListeners property of Observable
        // (see the constructor of Ext.mixin.Observable)
        me.data = me.data = data || (data = {});
        me.internalId = internalId = modelIdentifier.generate();
 
        //<debug>
        var dataId = data[idProperty]; // eslint-disable-line vars-on-top, one-var
        
        if (session && !session.isSession) {
            Ext.raise('Bad Model constructor argument 2 - "session" is not a Session');
        }
        //</debug>
 
        if ((array = data) instanceof Array) {
            me.data = data = {};
            fields = me.getFields();
            len = Math.min(fields.length, array.length);
            
            for (= 0; i < len; ++i) {
                data[fields[i].name] = array[i];
            }
        }
 
        if (!(initializeFn = cls.initializeFn)) {
            cls.initializeFn = initializeFn = Model.makeInitializeFn(cls);
        }
        
        if (!initializeFn.$nullFn) {
            cls.initializeFn(me);
        }
 
        // Must do this after running the initializeFn due to converters on idField
        if (!me.isSummaryModel) {
            if (!(me.id = id = data[idProperty]) && id !== 0) {
                //<debug>
                if (dataId) {
                    Ext.raise('The model ID configured in data ("' + dataId +
                              '") has been rejected by the ' + me.fieldsMap[idProperty].type +
                              ' field converter for the ' + idProperty + ' field');
                }
                //</debug>
                
                if (session) {
                    identifier = session.getIdentifier(cls);
                    id = identifier.generate();
                }
                else if (modelIdentifier === identifier) {
                    id = internalId;
                }
                else {
                    id = identifier.generate();
                }
 
                data[idProperty] = me.id = id;
                me.phantom = true;
                me.crudState = 'C';
            }
 
            if (session && !skipStoreAddition) {
                session.add(me);
            }
 
            // Needs to be set after the add to the session
            if (me.phantom) {
                me.crudStateWas = 'C';
            }
        }
 
        if (me.init && Ext.isFunction(me.init)) {
            me.init();
        }
    },
 
    /**
     * @property {String} entityName
     * The short name of this entity class. This name is derived from the `namespace` of
     * the associated `schema` and this class name. By default, a class is not given a
     * shortened name.
     *
     * All entities in a given `schema` must have a unique `entityName`.
     * 
     * For more details see "Relative Naming" in {@link Ext.data.schema.Schema}.
     */
 
    /**
     * @property {Boolean} editing
     * Internal flag used to track whether or not the model instance is currently being edited.
     * @readonly
     */
    editing: false,
 
    /**
     * @property {Boolean} dirty
     * True if this record has been modified.
     * @readonly
     */
    dirty: false,
 
    /**
     * @property {Ext.data.Session} session
     * The {@link Ext.data.Session} for this record.
     * @readonly
     */
    session: null,
 
    /**
     * @property {Boolean} dropped
     * True if this record is pending delete on the server. This is set by the `drop`
     * method and transmitted to the server by the `save` method.
     * @readonly
     */
    dropped: false,
    
    /**
     * @property {Boolean} erased
     * True if this record has been erased on the server. This flag is set of the `erase`
     * method.
     * @readonly
     */
    erased: false,
 
    /**
     * @cfg {String} clientIdProperty
     * The name of the property a server will use to send back a client-generated id in a
     * `create` or `update` `{@link Ext.data.operation.Operation operation}`.
     *
     * If specified, this property cannot have the same name as any other field.
     *
     * For example:
     *
     *      Ext.define('Person', {
     *          idProperty: 'id',  // this is the default value (for clarity)
     *
     *          clientIdProperty: 'clientId',
     *
     *          identifier: 'negative', // to generate -1, -2 etc on the client
     *
     *          fields: [ 'name' ]
     *      });
     *
     *      var person = new Person({
     *          // no id provided, so -1 is generated
     *          name: 'Clark Kent'
     *      });
     *
     * The server is given this data during the `create`:
     *
     *      {
     *          id: -1,
     *          name: 'Clark Kent'
     *      }
     *
     * The server allocates a real id and responds like so:
     *
     *      {
     *          id: 427,
     *          clientId: -1
     *      }
     *
     * This property is most useful when creating multiple entities in a single call to
     * the server in a `{@link Ext.data.operation.Create create operation}`. Alternatively,
     * the server could respond with records that correspond one-to-one to those sent in
     * the `operation`.
     *
     * For example the client could send a `create` with this data:
     *
     *      [ { id: -1, name: 'Clark Kent' },
     *        { id: -2, name: 'Peter Parker' },
     *        { id: -3, name: 'Bruce Banner' } ]
     *
     * And the server could respond in the same order:
     *
     *      [ { id: 427 },      // updates id = -1
     *        { id: 428 },      // updates id = -2
     *        { id: 429 } ]     // updates id = -3
     *
     * Or using `clientIdProperty` the server could respond in arbitrary order:
     *
     *      [ { id: 427, clientId: -3 },
     *        { id: 428, clientId: -1 },
     *        { id: 429, clientId: -2 } ]
     *
     * **IMPORTANT:** When upgrading from previous versions be aware that this property
     * used to perform the role of `{@link Ext.data.writer.Writer#clientIdProperty}` as
     * well as that described above. To continue send a client-generated id as other than
     * the `idProperty`, set `clientIdProperty` on the `writer`. A better solution, however,
     * is most likely a properly configured `identifier` as that would work better with
     * associations.
     */
    clientIdProperty: null,
 
    evented: false,
 
    /**
     * @property {Boolean} phantom
     * True when the record does not yet exist in a server-side database. Any record which
     * has a real database identity set as its `idProperty` is NOT a phantom -- it's real.
     */
    phantom: false,
 
    /**
     * @cfg {String} idProperty
     * The name of the field treated as this Model's unique id.
     *
     * If changing the idProperty in a subclass, the generated id field will replace the
     * one generated by the superclass, for example;
     *
     *      Ext.define('Super', {
     *          extend: 'Ext.data.Model',
     *          fields: ['name']
     *      });
     *
     *      Ext.define('Sub', {
     *          extend: 'Super',
     *          idProperty: 'customId'
     *      });
     *
     *      var fields = Super.getFields();
     *      // Has 2 fields, "name" & "id"
     *      console.log(fields[0].name, fields[1].name, fields.length);
     *
     *      fields = Sub.getFields();
     *      // Has 2 fields, "name" & "customId", "id" is replaced
     *      console.log(fields[0].name, fields[1].name, fields.length);
     *
     * The data values for this field must be unique or there will be id value collisions
     * in the {@link Ext.data.Store Store}.
     */
    idProperty: 'id',
 
    /**
     * @cfg {Object} manyToMany
     * A config object for a {@link Ext.data.schema.ManyToMany ManyToMany} association.
     * See the class description for {@link Ext.data.schema.ManyToMany ManyToMany} for
     * configuration examples.
     */
    manyToMany: null,
 
    /**
     * @cfg {String/Object} identifier
     * The id generator to use for this model. The `identifier` generates values for the
     * {@link #idProperty} when no value is given. Records with client-side generated
     * values for {@link #idProperty} are called {@link #phantom} records since they are
     * not yet known to the server.
     *
     * This can be overridden at the model level to provide a custom generator for a
     * model. The simplest form of this would be:
     *
     *      Ext.define('MyApp.data.MyModel', {
     *          extend: 'Ext.data.Model',
     *          requires: ['Ext.data.identifier.Sequential'],
     *          identifier: 'sequential',
     *          ...
     *      });
     *
     * The above would generate {@link Ext.data.identifier.Sequential sequential} id's
     * such as 1, 2, 3 etc..
     *
     * Another useful id generator is {@link Ext.data.identifier.Uuid}:
     *
     *      Ext.define('MyApp.data.MyModel', {
     *          extend: 'Ext.data.Model',
     *          requires: ['Ext.data.identifier.Uuid'],
     *          identifier: 'uuid',
     *          ...
     *      });
     *
     * An id generator can also be further configured:
     *
     *      Ext.define('MyApp.data.MyModel', {
     *          extend: 'Ext.data.Model',
     *          identifier: {
     *              type: 'sequential',
     *              seed: 1000,
     *              prefix: 'ID_'
     *          }
     *      });
     *
     * The above would generate id's such as ID_1000, ID_1001, ID_1002 etc..
     *
     * If multiple models share an id space, a single generator can be shared:
     *
     *      Ext.define('MyApp.data.MyModelX', {
     *          extend: 'Ext.data.Model',
     *          identifier: {
     *              type: 'sequential',
     *              id: 'xy'
     *          }
     *      });
     *
     *      Ext.define('MyApp.data.MyModelY', {
     *          extend: 'Ext.data.Model',
     *          identifier: {
     *              type: 'sequential',
     *              id: 'xy'
     *          }
     *      });
     *
     * For more complex, shared id generators, a custom generator is the best approach.
     * See {@link Ext.data.identifier.Generator} for details on creating custom id
     * generators.
     */
    identifier: null,
 
    // Fields config and property
    // @cmd-auto-dependency {aliasPrefix: "data.field."}
    /**
     * @cfg {Object[]/String[]} fields
     * An Array of `Ext.data.field.Field` config objects, simply the field 
     * {@link Ext.data.field.Field#name name}, or a mix of config objects and strings. 
     * If just a name is given, the field type defaults to `auto`.
     * 
     * In a {@link Ext.data.field.Field Field} config object you may pass the alias of 
     * the `Ext.data.field.*` type using the `type` config option.
     * 
     *     // two fields are set:
     *     // - an 'auto' field with a name of 'firstName'
     *     // - and an Ext.data.field.Integer field with a name of 'age'
     *     fields: ['firstName', {
     *         type: 'int',
     *         name: 'age'
     *     }]
     * 
     * Fields will automatically be created at read time for any for any keys in the 
     * data passed to the Model's {@link #proxy proxy's} 
     * {@link Ext.data.reader.Reader reader} whose name is not explicitly configured in 
     * the `fields` config.
     * 
     * Extending a Model class will inherit all the `fields` from the superclass / 
     * ancestor classes.
     */
    /**
     * @property {Ext.data.field.Field[]} fields
     * An array fields defined for this Model (including fields defined in superclasses)
     * in ordinal order; that is in declaration order.
     * @private
     * @readonly
     */
 
    /**
     * @property {Object} fieldOrdinals
     * This property is indexed by field name and contains the ordinal of that field. The
     * ordinal often has meaning to servers and is derived based on the position in the
     * `fields` array.
     * 
     * This can be used like so:
     * 
     *      Ext.define('MyApp.models.User', {
     *          extend: 'Ext.data.Model',
     *
     *          fields: [
     *              { name: 'name' }
     *          ]
     *      });
     * 
     *      var nameOrdinal = MyApp.models.User.fieldOrdinals.name;
     *      
     *      // or, if you have an instance:
     *
     *      var user = new MyApp.models.User();
     *      var nameOrdinal = user.fieldOrdinals.name;
     *
     * @private
     * @readonly
     */
 
    /**
      * @property {Object} modified
      * A hash of field values which holds the initial values of fields before a set of
      * edits are {@link #commit committed}.
      */
 
    /**
     * @property {Object} previousValues
     * This object is similar to the `modified` object except it holds the data values as
     * they were prior to the most recent change.
     * @readonly
     * @private
     */
    previousValues: undefined, // Not "null" so getPrevious returns undefined first time
 
    // @cmd-auto-dependency { aliasPrefix : "proxy.", defaultPropertyName : "defaultProxyType"}
    /**
     * @cfg {String/Object/Ext.data.proxy.Proxy} proxy
     * The {@link Ext.data.proxy.Proxy proxy} to use for this class.
     *
     * By default, the proxy is configured from the {@link Ext.data.schema.Schema schema}.
     * You can ignore the schema defaults by setting `schema: false` on the `proxy` config.
     *
     *      Ext.define('MyApp.data.CustomProxy', {
     *          extend: 'Ext.data.proxy.Ajax',
     *          alias: 'proxy.customproxy',
     *
     *          url: 'users.json'
     *      });
     *
     *      Ext.define('MyApp.models.CustomModel', {
     *          extend: 'Ext.data.Model',
     *
     *          fields: ['name'],
     *          proxy: {
     *              type: 'customproxy,
     *              schema: false
     *          }
     *      });
     *
     * With `schema: false`, the `url` of the proxy will be used instead of what has been defined
     * on the schema.
     */
    proxy: undefined,
 
    /**
     * @cfg {String/Object} [schema='default']
     * The name of the {@link Ext.data.schema.Schema schema} to which this entity and its
     * associations belong. For details on custom schemas see `Ext.data.schema.Schema`.
     */
    /**
     * @property {Ext.data.schema.Schema} schema
     * The `Ext.data.schema.Schema` to which this entity and its associations belong.
     * @readonly
     */
    schema: 'default',
 
    /**
     * @cfg {Object} summary
     * Summary fields are a special kind of field that is used to assist in creating an
     * aggregation for this model. A new model type that extends this model will be
     * created, accessible via {@link #method-getSummaryModel}. This summary model will
     * have these virtual aggregate fields in the fields collection like a normal model.
     * Each key in the object is the field name. The value for each field should mirror
     * the {@link #cfg-fields}, excluding the `name` option. The summary model generated
     * will have 2 fields, 'rate', which will aggregate using an average and maxRate,
     * which will aggregate using the maximum value.
     *
     * See {@link Ext.data.summary.Base} for more information.
     *
     *      Ext.define('User', {
     *          extend: 'Ext.data.Model',
     *          fields: [{
     *              name: 'rate',
     *              summary: 'avg'
     *          }],
     *
     *          summary: {
     *              maxRate: {
     *                  field: 'rate', // calculated from rate field
     *                  summary: 'max'
     *              }
     *          }
     *      });
     *
     * @since 6.5.0
     */
    summary: null,
 
    /**
     * @cfg {String} versionProperty
     * If specified, this is the name of the property that contains the entity "version".
     * The version property is used to manage a long-running transaction and allows the
     * detection of simultaneous modification.
     * 
     * The way a version property is used is that the client receives the version as it
     * would any other entity property. When saving an entity, this property is always
     * included in the request and the server uses the value in a "conditional update".
     * If the current version of the entity on the server matches the version property
     * sent by the client, the update is allowed. Otherwise, the update fails.
     * 
     * On successful update, both the client and server increment the version. This is
     * done on the server in the conditional update and on the client when it receives a
     * success on its update request.
     */
    versionProperty: null,
 
    /**
     * @property {Number} generation
     * This property is incremented on each modification of a record.
     * @readonly
     * @since 5.0.0
     */
    generation: 1,
 
    /**
     * @cfg {Object[]} validators
     * An array of {@link Ext.data.validator.Validator validators} for this model.
     */
 
    /**
     * @cfg {String} validationSeparator
     * If specified this property is used to concatenate multiple errors for each field
     * as reported by the `validators`.
     */
    validationSeparator: null,
 
    /**
     * @cfg {Boolean} convertOnSet
     * Set to `false` to prevent any converters from being called on fields specified in 
     * a {@link Ext.data.Model#set set} operation.
     * 
     * **Note:** Setting the config to `false` will only prevent the convert / calculate 
     * call when the set `fieldName` param matches the field's `{@link #name}`.  In the 
     * following example the calls to set `salary` will not execute the convert method 
     * on `set` while the calls to set `vested` will execute the convert method on the 
     * initial read as well as on `set`.
     * 
     * Example model definition:
     * 
     *     Ext.define('MyApp.model.Employee', {
     *         extend: 'Ext.data.Model',
     *         fields: ['yearsOfService', {
     *             name: 'salary',
     *             convert: function (val) {
     *                 var startingBonus = val * .1;
     *                 return val + startingBonus;
     *             }
     *         }, {
     *             name: 'vested',
     *             convert: function (val, record) {
     *                 return record.get('yearsOfService') >= 4;
     *             },
     *             depends: 'yearsOfService'
     *         }],
     *         convertOnSet: false
     *     });
     *     
     *     var tina = Ext.create('MyApp.model.Employee', {
     *         salary: 50000,
     *         yearsOfService: 3
     *     });
     *     
     *     console.log(tina.get('salary')); // logs 55000
     *     console.log(tina.get('vested')); // logs false
     *     
     *     tina.set({
     *         salary: 60000,
     *         yearsOfService: 4
     *     });
     *     console.log(tina.get('salary')); // logs 60000
     *     console.log(tina.get('vested')); // logs true
     */
    convertOnSet: true,
 
    // Associations configs and properties
    /**
     * @cfg {Object[]} associations
     * An array of {@link Ext.data.schema.Association associations} for this model.
     *
     * For further documentation, see {@link Ext.data.schema.Association}.
     *
     * @deprecated 6.2.0 Use `hasMany/hasOne/belongsTo`.
     */
 
    /**
     * @cfg {String/Object/String[]/Object[]} hasMany
     * One or more `Ext.data.schema.HasMany` associations for this model.
     */
 
    /**
     * @cfg {String/Object/String[]/Object[]} hasOne
     * One or more `Ext.data.schema.HasOne` associations for this model.
     */
 
    /**
     * @cfg {String/Object/String[]/Object[]} belongsTo
     * One or more `Ext.data.schema.BelongsTo` associations for this model.
     */
 
    /**
     * Begins an edit. While in edit mode, no events (e.g.. the `update` event) are
     * relayed to the containing store. When an edit has begun, it must be followed by
     * either `endEdit` or `cancelEdit`.
     */
    beginEdit: function() {
        var me = this,
            modified = me.modified,
            previousValues = me.previousValues;
 
        if (!me.editing) {
            me.editing = true;
 
            me.editMemento = {
                dirty: me.dirty,
                data: Ext.apply({}, me.data),
                generation: me.generation,
                modified: modified && Ext.apply({}, modified),
                previousValues: previousValues && Ext.apply({}, previousValues)
            };
        }
    },
 
    /**
     * Calculate all summary fields on this record.
     * @param {Ext.data.Model[]} records The records to use for calculation.
     *
     * @since 6.5.0
     */
    calculateSummary: function(records) {
        var fields = this.getFields(),
            len = fields.length,
            recLen = records.length,
            i, result, summary, prop, name, field;
 
        for (= 0; i < len; ++i) {
            field = fields[i];
            summary = field.getSummary();
            
            if (summary) {
                result = result || {};
                name = field.name;
                prop = field.summaryField || name;
                result[name] = summary.calculate(records, prop, 'data', 0, recLen);
            }
        }
 
        if (result) {
            this.set(result, this._commitOptions);
        }
    },
 
    /**
     * Cancels all changes made in the current edit operation.
     */
    cancelEdit: function() {
        var me = this,
            editMemento = me.editMemento,
            validation = me.validation;
 
        if (editMemento) {
            me.editing = false;
 
            // reset the modified state, nothing changed since the edit began
            Ext.apply(me, editMemento);
            me.editMemento = null;
 
            if (validation && validation.syncGeneration !== me.generation) {
                validation.syncGeneration = 0;
            }
        }
    },
 
    /**
     * Ends an edit. If any data was modified, the containing store is notified
     * (ie, the store's `update` event will fire).
     * @param {Boolean} [silent] True to not notify any stores of the change.
     * @param {String[]} [modifiedFieldNames] Array of field names changed during edit.
     */
    endEdit: function(silent, modifiedFieldNames) {
        var me = this,
            editMemento = me.editMemento;
 
        if (editMemento) {
            me.editing = false;
            me.editMemento = null;
 
            // Since these reflect changes we never notified others about, the real set
            // of "previousValues" is what we captured in the memento:
            me.previousValues = editMemento.previousValues;
 
            if (!silent) {
                if (!modifiedFieldNames) {
                    modifiedFieldNames = me.getModifiedFieldNames(editMemento.data);
                }
 
                if (me.dirty || (modifiedFieldNames && modifiedFieldNames.length)) {
                    me.callJoined('afterEdit', [modifiedFieldNames]);
                }
            }
        }
    },
 
    getField: function(name) {
        return this.self.getField(name);
    },
 
    /**
     * Get the fields array for this model.
     * @return {Ext.data.field.Field[]} The fields array
     */
    getFields: function() {
        return this.self.getFields();
    },
 
    getFieldsMap: function() {
        return this.fieldsMap;
    },
 
    /**
     * Get the idProperty for this model.
     * @return {String} The idProperty
     */
    getIdProperty: function() {
        return this.idProperty;
    },
 
    /**
     * Returns the unique ID allocated to this model instance as defined by `idProperty`.
     * @return {Number/String} The id
     */
    getId: function() {
        return this.id;
    },
 
    /**
     * Return a unique observable ID. Model is not observable but tree nodes
     * (`Ext.data.NodeInterface`) are, so they must be globally unique within the
     * {@link #observableType}.
     * @protected
     */
    getObservableId: function() {
        return this.internalId;
    },
 
    /**
     * Sets the model instance's id field to the given id.
     * @param {Number/String} id The new id.
     * @param {Object} [options] See {@link #set}.
     */
    setId: function(id, options) {
        this.set(this.idProperty, id, options);
    },
 
    /**
     * This method returns the value of a field given its name prior to its most recent
     * change.
     * @param {String} fieldName The field's {@link Ext.data.field.Field#name name}.
     * @return {Object} The value of the given field prior to its current value. `undefined`
     * if there is no previous value;
     */
    getPrevious: function(fieldName) {
        var previousValues = this.previousValues;
        
        return previousValues && previousValues[fieldName];
    },
 
    /**
     * Returns true if the passed field name has been `{@link #modified}` since the load
     * or last commit.
     * @param {String} fieldName The field's {@link Ext.data.field.Field#name name}.
     * @return {Boolean} 
     */
    isModified: function(fieldName) {
        var modified = this.modified;
        
        return !!(modified && modified.hasOwnProperty(fieldName));
    },
    
    /**
     * Returns the original value of a modified field. If there is no modified value,
     * `undefined` will be return. Also see {@link #isModified}.
     * @param {String} fieldName The name of the field for which to return the original value.
     * @return {Object} modified
     */
    getModified: function(fieldName) {
        var out;
        
        if (this.isModified(fieldName)) {
            out = this.modified[fieldName];
        }
        
        return out;
    },
 
    /**
     * Returns the value of the given field.
     * @param {String} fieldName The name of the field.
     * @return {Object} The value of the specified field.
     */
    get: function(fieldName) {
        return this.data[fieldName];
    },
 
    // This object is used whenever the set() method is called and given a string as the
    // first argument. This approach saves memory (and GC costs) since we could be called
    // a lot.
    _singleProp: {},
 
    _rejectOptions: {
        convert: false,
        silent: true
    },
 
    /**
     * Sets the given field to the given value. For example:
     * 
     *      record.set('name', 'value');
     * 
     * This method can also be passed an object containing multiple values to set at once.
     * For example:
     * 
     *      record.set({
     *          name: 'value',
     *          age: 42
     *      });
     * 
     * The following store events are fired when the modified record belongs to a store:
     *
     *  - {@link Ext.data.Store#event-beginupdate beginupdate}
     *  - {@link Ext.data.Store#event-update update}
     *  - {@link Ext.data.Store#event-endupdate endupdate}
     * 
     * @param {String/Object} fieldName The field to set, or an object containing key/value 
     * pairs.
     * @param {Object} newValue The value for the field (if `fieldName` is a string).
     * @param {Object} [options] Options for governing this update.
     * @param {Boolean} [options.convert=true] Set to `false` to  prevent any converters from 
     * being called during the set operation. This may be useful when setting a large bunch of 
     * raw values.
     * @param {Boolean} [options.dirty=true] Pass `false` if the field values are to be
     * understood as non-dirty (fresh from the server). When `true`, this change will be
     * reflected in the `modified` collection.
     * @param {Boolean} [options.commit=false] Pass `true` to call the {@link #commit} method 
     * after setting fields. If this option is passed, the usual after change processing will 
     * be bypassed. {@link #commit Commit} will be called even if there are no field changes.
     * @param {Boolean} [options.silent=false] Pass `true` to suppress notification of any
     * changes made by this call. Use with caution.
     * @return {String[]} The array of modified field names or null if nothing was modified.
     */
    set: function(fieldName, newValue, options) {
        var me = this,
            cls = me.self,
            data = me.data,
            modified = me.modified,
            prevVals = me.previousValues,
            session = me.session,
            single = Ext.isString(fieldName),
            opt = (single ? options : newValue),
            convertOnSet = opt ? opt.convert !== false : me.convertOnSet,
            fieldsMap = me.fieldsMap,
            silent = opt && opt.silent,
            commit = opt && opt.commit,
            updateRefs = !(opt && opt.refs === false) && session,
            // Don't need to do dirty processing with commit, since we'll always
            // end up with nothing modified and not dirty
            dirty = !(opt && opt.dirty === false && !commit),
            modifiedFieldNames = null,
            dirtyRank = 0,
            associations = me.associations,
            currentValue, field, idChanged, key, name, oldId, comparator, dep, dependents,
            i, numFields, newId, rankedFields, reference, value, values, roleName;
 
        if (single) {
            values = me._singleProp;
            values[fieldName] = newValue;
        }
        else {
            values = fieldName;
        }
 
        if (!(rankedFields = cls.rankedFields)) {
            // On the first edit of a record of this type we need to ensure we have the
            // topo-sort done:
            rankedFields = cls.rankFields();
        }
        
        numFields = rankedFields.length;
 
        do {
            for (name in values) {
                value = values[name];
                currentValue = data[name];
                comparator = me;
                field = fieldsMap[name];
 
                if (field) {
                    if (convertOnSet && field.convert) {
                        value = field.convert(value, me);
                    }
                    
                    comparator = field;
                    reference = field.reference;
                }
                else {
                    reference = null;
                }
 
                if (comparator.isEqual(currentValue, value)) {
                    continue; // new value is the same, so no change...
                }
 
                data[name] = value;
                (modifiedFieldNames || (modifiedFieldNames = [])).push(name);
                (prevVals || (me.previousValues = prevVals = {}))[name] = currentValue;
 
                // We need the cls to be present because it means the association class is loaded,
                // otherwise it could be pending.
                if (reference && reference.cls) {
                    if (updateRefs) {
                        session.updateReference(me, field, value, currentValue);
                    }
                    
                    reference.onValueChange(me, session, value, currentValue);
                }
 
                i = (dependents = field && field.dependents) && dependents.length;
                
                while (i-- > 0) {
                    // we use the field instance to hold the dirty bit to avoid any
                    // extra allocations... we'll clear this before we depart. We do
                    // this so we can perform the fewest recalculations possible as
                    // each dependent field only needs to be recalculated once.
                    (dep = dependents[i]).dirty = true;
                    dirtyRank = dirtyRank ? Math.min(dirtyRank, dep.rank) : dep.rank;
                }
 
                if (!field || field.persist) {
                    if (modified && modified.hasOwnProperty(name)) {
                        if (!dirty || comparator.isEqual(modified[name], value)) {
                            // The original value in me.modified equals the new value, so
                            // the field is no longer modified:
                            delete modified[name];
                            me.dirty = -1; // fix me.dirty later (still truthy)
                        }
                    }
                    else if (dirty) {
                        if (!modified) {
                            me.modified = modified = {}; // create only when needed
                        }
                        
                        me.dirty = true;
                        modified[name] = currentValue;
                    }
                }
 
                if (name === me.idField.name) {
                    idChanged = true;
                    oldId = currentValue;
                    newId = value;
                }
            }
 
            if (!dirtyRank) {
                // Unless there are dependent fields to process we can break now. This is
                // what will happen for all code pre-dating the depends or simply not
                // using it, so it will add very little overhead when not used.
                break;
            }
 
            // dirtyRank has the minimum rank (a 1-based value) of any dependent field
            // that needs recalculating due to changes above. The way we go about this
            // is to use our helper object for processing single argument invocations
            // to process just this one field. This is because the act of setting it
            // may cause another field to be invalidated, so while we cannot know at
            // this moment all the fields we need to recalculate, we know that only
            // those following this field in rankedFields can possibly be among them.
 
            field = rankedFields[dirtyRank - 1]; // dirtyRank is 1-based
            field.dirty = false; // clear just this field's dirty state
 
            if (single) {
                delete values[fieldName]; // cleanup last value
            }
            else {
                values = me._singleProp; // switch over
                single = true;
            }
 
            fieldName = field.name;
            values[fieldName] = data[fieldName];
            // We are now processing a dependent field, so we want to force a
            // convert to occur because it's the only way it will get a value
            convertOnSet = true;
 
            // Since dirtyRank is 1-based and refers to the field we need to handle
            // on this pass, we can treat it like an index for a minute and look at
            // the next field on towards the end to find the index of the next dirty
            // field.
            for (; dirtyRank < numFields; ++dirtyRank) {
                if (rankedFields[dirtyRank].dirty) {
                    break;
                }
            }
 
            if (dirtyRank < numFields) {
                // We found a field after this one marked as dirty so make the index
                // a proper 1-based rank:
                ++dirtyRank;
            }
            else {
                // We did not find any more dirty fields after this one, so clear the
                // dirtyRank and we will perhaps fall out after the next update
                dirtyRank = 0;
            }
        } while (1); // eslint-disable-line no-constant-condition
 
        if (me.dirty < 0) {
            // We might have removed the last modified field, so check to see if there
            // are any modified fields remaining and correct me.dirty:
            me.dirty = false;
            
            for (key in modified) {
                if (modified.hasOwnProperty(key)) {
                    me.dirty = true;
                    
                    break;
                }
            }
        }
 
        if (single) {
            // cleanup our reused object for next time... important to do this before
            // we fire any events or call anyone else (like afterEdit)!
            delete values[fieldName];
        }
 
        ++me.generation;
 
        if (idChanged) {
            me.id = newId;
            me.onIdChanged(newId, oldId);
            me.callJoined('onIdChanged', [oldId, newId]);
            
            if (associations) {
                for (roleName in associations) {
                    associations[roleName].onIdChanged(me, oldId, newId);
                }
            }
        }
 
        if (commit) {
            me.commit(silent, modifiedFieldNames);
        }
        else if (!silent && !me.editing && modifiedFieldNames) {
            me.callJoined('afterEdit', [modifiedFieldNames]);
        }
 
        return modifiedFieldNames;
    },
 
    /**
     * Usually called by the {@link Ext.data.Store} to which this model instance has been
     * {@link #join joined}. Rejects all changes made to the model instance since either creation,
     * or the last commit operation. Modified fields are reverted to their original values.
     *
     * Developers should subscribe to the {@link Ext.data.Store#event-update} event to have their
     * code notified of reject operations.
     *
     * @param {Boolean} [silent=false] `true` to skip notification of the owning store of the
     * change.
     */
    reject: function(silent) {
        var me = this,
            modified = me.modified;
 
        //<debug>
        if (me.erased) {
            Ext.raise('Cannot reject once a record has been erased.');
        }
        //</debug>
 
        if (modified) {
            me.set(modified, me._rejectOptions);
        }
 
        me.dropped = false;
        me.clearState();
 
        if (!silent) {
            me.callJoined('afterReject');
        }
    },
    
    /**
     * Usually called by the {@link Ext.data.Store} which owns the model instance. Commits all
     * changes made to the instance since either creation or the last commit operation.
     *
     * Developers should subscribe to the {@link Ext.data.Store#event-update} event to have their
     * code notified of commit operations.
     *
     * @param {Boolean} [silent=false] Pass `true` to skip notification of the owning store of the
     * change.
     * @param {String[]} [modifiedFieldNames] Array of field names changed during sync with server
     * if known. Omit or pass `null` if unknown. An empty array means that it is known that
     * no fields were modified by the server's response.
     * Defaults to false.
     */
    commit: function(silent, modifiedFieldNames) {
        var me = this,
            versionProperty = me.versionProperty,
            data = me.data,
            erased;
 
        me.clearState();
        
        if (versionProperty && !me.phantom && !isNaN(data[versionProperty])) {
            ++data[versionProperty];
        }
        
        me.phantom = false;
 
        if (me.dropped) {
            me.erased = erased = true;
        }
 
        if (!silent) {
            if (erased) {
                me.callJoined('afterErase');
            }
            else {
                me.callJoined('afterCommit', [modifiedFieldNames]);
            }
        }
    },
    
    clearState: function() {
        var me = this;
        
        me.dirty = me.editing = false;
        me.editMemento = me.modified = null;
    },
 
    /**
     * Marks this record as `dropped` and waiting to be deleted on the server. When a
     * record is dropped, it is automatically removed from all association stores and
     * any child records associated to this record are also dropped (a "cascade delete")
     * depending on the `cascade` parameter.
     *
     * @param {Boolean} [cascade=true] Pass `false` to disable the cascade to drop child
     * records.
     * @since 5.0.0
     */
    drop: function(cascade) {
        var me = this,
            associations = me.associations,
            session = me.session,
            roleName;
 
        if (me.erased || me.dropped) {
            return;
        }
        
        me.dropped = true;
        
        if (associations && cascade !== false) {
            for (roleName in associations) {
                associations[roleName].onDrop(me, session);
            }
        }
        
        me.callJoined('afterDrop');
        
        if (me.phantom) {
            me.setErased();
        }
    },
 
    /**
     * Tells this model instance that an observer is looking at it.
     * @param {Ext.data.Store} owner The store or other owner object to which this model
     * has been added.
     */
    join: function(owner) {
        var me = this,
            joined = me.joined;
 
        // Optimize this, gets called a lot
        if (!joined) {
            joined = me.joined = [owner];
        }
        else if (!joined.length) {
            joined[0] = owner;
        }
        else {
            // TODO: do we need joined here? Perhaps push will do.
            Ext.Array.include(joined, owner);
        }
 
        if (owner.isStore && !me.store) {
            /**
            * @property {Ext.data.Store} store
            * The {@link Ext.data.Store Store} to which this instance belongs.
            *
            * **Note:** If this instance is bound to multiple stores, this property
            * will reference only the first.
            */
            me.store = owner;
        }
    },
 
    /**
     * Tells this model instance that it has been removed from the store.
     * @param {Ext.data.Store} owner The store or other owner object from which this
     * model has been removed.
     */
    unjoin: function(owner) {
        var me = this,
            joined = me.joined,
            
            // TreeModels are never joined to their TreeStore.
            // But unjoin is called by the base class's onCollectionRemove, so joined may be
            // undefined.
            len = joined && joined.length,
            store = me.store,
            i;
 
        if (owner === me.session) {
            me.session = null;
        }
        else {
            if (len === 1 && joined[0] === owner) {
                joined.length = 0;
            }
            else if (len) {
                Ext.Array.remove(joined, owner);
            }
 
            if (store === owner) {
                store = null;
                
                if (joined) {
                    for (= 0, len = joined.length; i < len; ++i) {
                        owner = joined[i];
                        
                        if (owner.isStore) {
                            store = owner;
                            
                            break;
                        }
                    }
                }
                
                me.store = store;
            }
        }
    },
 
    /**
     * Creates a clone of this record. States like `dropped`, `phantom` and `dirty` are
     * all preserved in the cloned record.
     *
     * @param {Ext.data.Session} [session] The session to which the new record
     * belongs.
     * @return {Ext.data.Model} The cloned record.
     */
    clone: function(session) {
        var me = this,
            modified = me.modified,
            ret = me.copy(me.id, session);
 
        if (modified) {
            // Restore the modified fields state
            ret.modified = Ext.apply({}, modified);
        }
 
        ret.dirty = me.dirty;
        ret.dropped = me.dropped;
        ret.phantom = me.phantom;
 
        return ret;
    },
 
    /**
     * Creates a clean copy of this record. The returned record will not consider any its
     * fields as modified.
     *
     * To generate a phantom instance with a new id pass `null`:
     *
     *     var rec = record.copy(null); // clone the record but no id (one is generated)
     *
     * @param {String} [newId] A new id, defaults to the id of the instance being copied.
     * See `{@link Ext.data.Model#idProperty idProperty}`.
     * @param {Ext.data.Session} [session] The session to which the new record
     * belongs.
     *
     * @return {Ext.data.Model} 
     */
    copy: function(newId, session) {
        var me = this,
            data = Ext.apply({}, me.data),
            idProperty = me.idProperty,
            T = me.self;
 
        if (newId || newId === 0) {
            data[idProperty] = newId;
        }
        else if (newId === null) {
            delete data[idProperty];
        }
 
        return new T(data, session);
    },
 
    /**
     * Returns the configured Proxy for this Model.
     * @return {Ext.data.proxy.Proxy} The proxy
     */
    getProxy: function() {
        return this.self.getProxy();
    },
 
    /**
     * Returns the `Ext.data.Validation` record holding the results of this record's
     * `validators`. This record is lazily created on first request and is then kept on
     * this record to be updated later.
     *
     * See the class description for more about `validators`.
     *
     * @param {Boolean} [refresh] Pass `false` to not call the `refresh` method on the
     * validation instance prior to returning it. Pass `true` to force a `refresh` of the
     * validation instance. By default the returned record is only refreshed if changes
     * have been made to this record.
     * @return {Ext.data.Validation} The `Validation` record for this record.
     * @since 5.0.0
     */
    getValidation: function(refresh) {
        var me = this,
            ret = me.validation;
 
        if (!ret) {
            me.validation = ret = new Ext.data.Validation();
            ret.attach(me);
        }
 
        if (refresh === true || (refresh !== false && ret.syncGeneration !== me.generation)) {
            ret.refresh(refresh);
        }
 
        return ret;
    },
 
    /**
     * Validates the current data against all of its configured {@link #validators}. The
     * returned collection holds an object for each reported problem from a `validator`.
     *
     * @return {Ext.data.ErrorCollection} The errors collection.
     * @deprecated 5.0 Use `getValidation` instead.
     */
    validate: function() {
        return new Ext.data.ErrorCollection().init(this);
    },
 
    /**
     * Checks if the model is valid. See {@link #getValidation}.
     * @return {Boolean} True if the model is valid.
     */
    isValid: function() {
        return this.getValidation().isValid();
    },
 
    /**
     * Returns a url-suitable string for this model instance. By default this just returns the
     * name of the Model class followed by the instance ID - for example an instance of
     * MyApp.model.User with ID 123 will return 'user/123'.
     * @return {String} The url string for this model instance.
     */
    toUrl: function() {
        var pieces = this.$className.split('.'),
            name = pieces[pieces.length - 1].toLowerCase();
 
        return name + '/' + this.getId();
    },
 
    /**
     * @method erase
     * @localdoc Destroys the model using the configured proxy.  The erase action is
     * asynchronous.  Any processing of the erased record should be done in a callback.
     *
     *     Ext.define('MyApp.model.User', {
     *         extend: 'Ext.data.Model',
     *         fields: [
     *             {name: 'id', type: 'int'},
     *             {name: 'name', type: 'string'}
     *         ],
     *         proxy: {
     *             type: 'ajax',
     *             url: 'server.url'
     *         }
     *     });
     *
     *     var user = new MyApp.model.User({
     *         name: 'Foo'
     *     });
     *
     *     // pass the phantom record data to the server to be saved
     *     user.save({
     *         success: function(record, operation) {
     *             // do something if the save succeeded
     *             // erase the created record
     *             record.erase({
     *                 failure: function(record, operation) {
     *                     // do something if the erase failed
     *                 },
     *                 success: function(record, operation) {
     *                     // do something if the erase succeeded
     *                 },
     *                 callback: function(record, operation, success) {
     *                     // do something if the erase succeeded or failed
     *                 }
     *             });
     *         }
     *     });
     *
     * **NOTE:** If a {@link #phantom} record is erased it will not be processed via the
     * proxy.  However, any passed `success` or `callback` functions will be called.
     *
     * The options param is an {@link Ext.data.operation.Destroy} config object
     * containing success, failure and callback functions, plus optional scope.
     *
     * @inheritdoc #method-load
     * @return {Ext.data.operation.Destroy} The destroy operation
     */
    erase: function(options) {
        var me = this;
 
        me.erasing = true;
 
        // Drop causes a removal from the backing Collection.
        // The store's onCollectionRemove will respond to this by adding the record to
        // its "to remove" stack and setting its needsSync
        // flag unless the above "erasing" flag is set.
        me.drop();
 
        me.erasing = false;
        
        return me.save(options);
    },
    
    setErased: function() {
        this.erased = true;
        this.callJoined('afterErase');
    },
 
    /**
     * Gets an object of only the fields that have been modified since this record was
     * created or committed. Only persistent fields are tracked in the `modified` set so
     * this method will only return changes to persistent fields.
     *
     * For more control over the returned data, see `{@link #getData}`.
     * @return {Object} 
     */
    getChanges: function() {
        return this.getData(this._getChangesOptions);
    },
 
    /**
     * Returns the array of fields that are declared as critical (must always send).
     * @return {Ext.data.field.Field[]} 
     */
    getCriticalFields: function() {
        var cls = this.self,
            ret = cls.criticalFields;
 
        if (!ret) {
            cls.rankFields();
            ret = cls.criticalFields;
        }
 
        return ret;
    },
 
    /**
     * This method is called by the {@link Ext.data.reader.Reader} after loading a model from
     * the server. This is after processing any inline associations that are available.
     * 
     * @method onLoad
     *
     * @protected
     * @template
     */
 
    /**
     * Gets all of the data from this Models *loaded* associations. It does this
     * recursively. For example if we have a User which hasMany Orders, and each Order
     * hasMany OrderItems, it will return an object like this:
     *
     *     {
     *         orders: [
     *             {
     *                 id: 123,
     *                 status: 'shipped',
     *                 orderItems: [
     *                     ...
     *                 ]
     *             }
     *         ]
     *     }
     *
     * @param {Object} [result] The object on to which the associations will be added. If
     * no object is passed one is created. This object is then returned.
     * @param {Boolean/Object} [options] An object containing options describing the data
     * desired.
     * @param {Boolean} [options.associated=true] Pass `true` to include associated data from
     * other associated records.
     * @param {Boolean} [options.changes=false] Pass `true` to only include fields that
     * have been modified. Note that field modifications are only tracked for fields that
     * are not declared with `persist` set to `false`. In other words, only persistent
     * fields have changes tracked so passing `true` for this means `options.persist` is
     * redundant.
     * @param {Boolean} [options.critical] Pass `true` to include fields set as `critical`.
     * This is only meaningful when `options.changes` is `true` since critical fields may
     * not have been modified.
     * @param {Boolean} [options.persist] Pass `true` to only return persistent fields.
     * This is implied when `options.changes` is set to `true`.
     * @param {Boolean} [options.serialize=false] Pass `true` to invoke the `serialize`
     * method on the returned fields.
     * @return {Object} The nested data set for the Model's loaded associations.
     */
    getAssociatedData: function(result, options) {
        var me = this,
            associations = me.associations,
            deep, i, item, items, itemData, length,
            record, role, roleName, opts, clear, associated;
 
        result = result || {};
 
        me.$gathering = 1;
 
        if (options) {
            options = Ext.apply({}, options);
        }
 
        for (roleName in associations) {
            role = associations[roleName];
            item = role.getAssociatedItem(me);
            
            if (!item || item.$gathering) {
                continue;
            }
 
            if (item.isStore) {
                item.$gathering = 1;
 
                items = item.getData().items; // get the records for the store
                length = items.length;
                itemData = [];
 
                for (= 0; i < length; ++i) {
                    // NOTE - we don't check whether the record is gathering here because
                    // we cannot remove it from the store (it would invalidate the index
                    // values and misrepresent the content). Instead we tell getData to
                    // only get the fields vs descend further.
                    record = items[i];
                    deep = !record.$gathering;
                    record.$gathering = 1;
                    
                    if (options) {
                        associated = options.associated;
                        
                        if (associated === undefined) {
                            options.associated = deep;
                            clear = true;
                        }
                        else if (!deep) {
                            options.associated = false;
                            clear = true;
                        }
                        
                        opts = options;
                    }
                    else {
                        opts = deep ? me._getAssociatedOptions : me._getNotAssociatedOptions;
                    }
                    
                    itemData.push(record.getData(opts));
                    
                    if (clear) {
                        options.associated = associated;
                        clear = false;
                    }
                    
                    delete record.$gathering;
                }
 
                delete item.$gathering;
            }
            else {
                opts = options || me._getAssociatedOptions;
                
                if (options && options.associated === undefined) {
                    opts.associated = true;
                }
                
                itemData = item.getData(opts);
            }
 
            result[roleName] = itemData;
        }
 
        delete me.$gathering;
 
        return result;
    },
 
    /**
     * Gets all values for each field in this model and returns an object containing the
     * current data. This can be tuned by passing an `options` object with various
     * properties describing the desired result. Passing `true` simply returns all fields
     * *and* all associated record data.
     *
     * To selectively gather some associated data, the `options` object can be used as
     * follows:
     *
     *      var data = order.getData({
     *          associated: {
     *              orderItems: true
     *          }
     *      });
     *
     * This will include all data fields as well as an "orderItems" array with the data
     * for each `OrderItem`. To include the associated `Item` for each `OrderItem`, the
     * call would look like:
     *
     *      var data = order.getData({
     *          associated: {
     *              orderItems: {
     *                  item: true
     *              }
     *          }
     *      });
     *
     * @param {Boolean/Object} [options] An object containing options describing the data
     * desired. If `true` is passed it is treated as an object with `associated` set to
     * `true`.
     * @param {Boolean/Object} [options.associated=false] Pass `true` to recursively
     * include all associated data. This is equivalent to pass `true` as the only argument.
     * See `getAssociatedData`. If `associated` is an object, it describes the specific
     * associations to gather.
     * @param {Boolean} [options.changes=false] Pass `true` to only include fields that
     * have been modified. Note that field modifications are only tracked for fields that
     * are not declared with `persist` set to `false`. In other words, only persistent
     * fields have changes tracked so passing `true` for this means `options.persist` is
     * redundant.
     * @param {Boolean} [options.critical] Pass `true` to include fields set as `critical`.
     * This is only meaningful when `options.changes` is `true` since critical fields may
     * not have been modified.
     * @param {Boolean} [options.persist] Pass `true` to only return persistent fields.
     * This is implied when `options.changes` is set to `true`.
     * @param {Boolean} [options.serialize=false] Pass `true` to invoke the `serialize`
     * method on the returned fields.
     * @return {Object} An object containing all the values in this model.
     */
    getData: function(options) {
        var me = this,
            ret = {},
            opts = (options === true) ? me._getAssociatedOptions : (options || ret), // cheat
            data = me.data,
            associated = opts.associated,
            changes = opts.changes,
            critical = changes && opts.critical,
            content = changes ? me.modified : data,
            fieldsMap = me.fieldsMap,
            persist = opts.persist,
            serialize = opts.serialize,
            criticalFields, field, n, name, value;
 
        // DON'T use "opts" from here on...
 
        // Keep in mind the two legacy use cases:
        //  - getData() ==> Ext.apply({}, me.data)
        //  - getData(true) ==> Ext.apply(Ext.apply({}, me.data), me.getAssociatedData())
 
        if (content) { // when processing only changes, me.modified could be null
            for (name in content) {
                value = data[name];
 
                field = fieldsMap[name];
                
                if (field) {
                    if (persist && !field.persist) {
                        continue;
                    }
                    
                    if (serialize && field.serialize) {
                        value = field.serialize(value, me);
                    }
                }
 
                ret[name] = value;
            }
        }
 
        if (critical) {
            criticalFields = me.self.criticalFields || me.getCriticalFields();
            
            for (= criticalFields.length; n-- > 0;) {
                name = (field = criticalFields[n]).name;
 
                if (!(name in ret)) {
                    value = data[name];
                    
                    if (serialize && field.serialize) {
                        value = field.serialize(value, me);
                    }
                    
                    ret[name] = value;
                }
            }
        }
 
        if (associated) {
            if (typeof associated === 'object') {
                me.getNestedData(opts, ret);
            }
            else {
                me.getAssociatedData(ret, opts);
            }
        }
 
        return ret;
    },
 
    getNestedData: function(options, result) {
        var me = this,
            associations = me.associations,
            graph = options.associated,
            i, item, items, itemData, length, record, role, roleName, opts;
 
        result = result || {};
 
        // For example:
        //
        //      associated: {
        //          orderItems: true
        //      }
        //
        //      associated: {
        //          orderItems: {
        //              item: true
        //          }
        //      }
        //
        for (roleName in graph) {
            role = associations[roleName];
            opts = graph[roleName];
            
            if (opts === true) {
                delete options.associated;
            }
            else {
                options.associated = opts;
            }
 
            item = role.getAssociatedItem(me);
            
            if (item.isStore) {
                items = item.getData().items; // get the records for the store
                length = items.length;
                itemData = [];
 
                for (= 0; i < length; ++i) {
                    record = items[i];
                    itemData.push(record.getData(options));
                }
            }
            else {
                itemData = item.getData(options);
            }
 
            result[roleName] = itemData;
        }
 
        options.associated = graph; // restore the original value
 
        return result;
    },
 
    /**
     * Returns the array of fields that are declared as non-persist or "transient".
     * @return {Ext.data.field.Field[]} 
     * @since 5.0.0
     */
    getTransientFields: function() {
        var cls = this.self,
            ret = cls.transientFields;
 
        if (!ret) {
            cls.rankFields(); // populates transientFields as well as rank
            ret = cls.transientFields;
        }
 
        return ret;
    },
 
    /**
     * Checks whether this model is loading data from the {@link #proxy}.
     * @return {Boolean} `true` if in a loading state.
     */
    isLoading: function() {
        return !!this.loadOperation;
    },
 
    /**
     * Aborts a pending {@link #method!load} operation. If the record is not loading, this does
     * nothing.
     */
    abort: function() {
        var operation = this.loadOperation;
        
        if (operation) {
            operation.abort();
        }
    },
 
    /**
     * @localdoc Loads the model instance using the configured proxy.  The load action
     * is asynchronous.  Any processing of the loaded record should be done in a
     * callback.
     *
     *     Ext.define('MyApp.model.User', {
     *         extend: 'Ext.data.Model',
     *         fields: [
     *             {name: 'id', type: 'int'},
     *             {name: 'name', type: 'string'}
     *         ],
     *         proxy: {
     *             type: 'ajax',
     *             url: 'server.url'
     *         }
     *     });
     *
     *     var user = new MyApp.model.User();
     *     user.load({
     *         scope: this,
     *         failure: function(record, operation) {
     *             // do something if the load failed
     *         },
     *         success: function(record, operation) {
     *             // do something if the load succeeded
     *         },
     *         callback: function(record, operation, success) {
     *             // do something whether the load succeeded or failed
     *         }
     *     });
     *
     * The options param is an {@link Ext.data.operation.Read} config object containing
     * success, failure and callback functions, plus optional scope.
     *
     * @param {Object} [options] Options to pass to the proxy.
     * @param {Function} options.success A function to be called when the
     * model is processed by the proxy successfully.
     * The callback is passed the following parameters:
     * @param {Ext.data.Model} options.success.record The record.
     * @param {Ext.data.operation.Operation} options.success.operation The operation.
     * 
     * @param {Function} options.failure A function to be called when the
     * model is unable to be processed by the server.
     * The callback is passed the following parameters:
     * @param {Ext.data.Model} options.failure.record The record.
     * @param {Ext.data.operation.Operation} options.failure.operation The operation.
     * 
     * @param {Function} options.callback A function to be called whether the proxy
     * transaction was successful or not.
     * The callback is passed the following parameters:
     * @param {Ext.data.Model} options.callback.record The record.
     * @param {Ext.data.operation.Operation} options.callback.operation The operation.
     * @param {Boolean} options.callback.success `true` if the operation was successful.
     * 
     * @param {Object} options.scope The scope in which to execute the callback
     * functions.  Defaults to the model instance.
     *
     * @return {Ext.data.operation.Read} The read operation.
     */
    load: function(options) {
        options = Ext.apply({}, options);
 
        /* eslint-disable-next-line vars-on-top */
        var me = this,
            scope = options.scope || me,
            proxy = me.getProxy(),
            callback = options.callback,
            operation = me.loadOperation,
            id = me.getId(),
            extras;
 
        if (operation) {
            // Already loading, push any callbacks on and jump out
            extras = operation.extraCalls;
            
            if (!extras) {
                extras = operation.extraCalls = [];
            }
            
            extras.push(options);
            
            return operation;
        }
 
        //<debug>
        var doIdCheck = true; // eslint-disable-line vars-on-top, one-var
        
        if (me.phantom) {
            doIdCheck = false;
        }
        //</debug>
 
        options.id = id;
 
        // Always set the recordCreator. If we have a session, we're already
        // part of said session, so we don't need to handle that.
        options.recordCreator = function(data, type, readOptions) {
            // Important to change this here, because we might be loading associations,
            // so we do not want this to propagate down. If we have a session, use that
            // so that we end up getting the same record. Otherwise, just remove it.
            var session = me.session;
            
            if (readOptions) {
                readOptions.recordCreator = session ? session.recordCreator : null;
            }
            
            me.set(data, me._commitOptions);
            
            //<debug>
            // Do the id check after set since converters may have run
            if (doIdCheck && me.getId() !== id) {
                Ext.raise('Invalid record id returned for ' + id + '@' + me.entityName);
            }
            //</debug>
            
            return me;
        };
 
        options.internalCallback = function(operation) {
            var success = operation.wasSuccessful() && operation.getRecords().length > 0,
                op = me.loadOperation,
                extras = op.extraCalls,
                successFailArgs = [me, operation],
                callbackArgs = [me, operation, success],
                i, len;
 
            me.loadOperation = null;
            ++me.loadCount;
 
            if (success) {
                Ext.callback(options.success, scope, successFailArgs);
            }
            else {
                Ext.callback(options.failure, scope, successFailArgs);
            }
            
            Ext.callback(callback, scope, callbackArgs);
 
            // Some code repetition here, however in a vast majority of cases
            // we'll only have a single callback, so optimize for that case rather
            // than setup arrays for all the callback options
            if (extras) {
                for (= 0, len = extras.length; i < len; ++i) {
                    options = extras[i];
                    
                    if (success) {
                        Ext.callback(options.success, scope, successFailArgs);
                    }
                    else {
                        Ext.callback(options.failure, scope, successFailArgs);
                    }
                    
                    Ext.callback(options.callback, scope, callbackArgs);
                }
            }
            
            me.callJoined('afterLoad');
        };
        
        delete options.callback;
 
        me.loadOperation = operation = proxy.createOperation('read', options);
        operation.execute();
 
        return operation;
    },
 
    /**
     * Merge incoming data from the server when this record exists
     * in an active session. This method is not called if this record is
     * loaded directly via {@link #method!load}. The default behaviour is to use incoming
     * data if the record is not {@link #dirty}, otherwise the data is
     * discarded. This method should be overridden in subclasses to
     * provide a different behavior.
     * @param {Object} data The model data retrieved from the server.
     *
     * @protected
     *
     * @since 6.5.0
     */
    mergeData: function(data) {
        if (!this.dirty) {
            this.set(data, this._commitOptions);
        }
    },
 
    /**
     * @method save
     * @localdoc Saves the model instance using the configured proxy.  The save action
     * is asynchronous.  Any processing of the saved record should be done in a callback.
     *
     * Create example:
     *
     *     Ext.define('MyApp.model.User', {
     *         extend: 'Ext.data.Model',
     *         fields: [
     *             {name: 'id', type: 'int'},
     *             {name: 'name', type: 'string'}
     *         ],
     *         proxy: {
     *             type: 'ajax',
     *             url: 'server.url'
     *         }
     *     });
     *
     *     var user = new MyApp.model.User({
     *         name: 'Foo'
     *     });
     *
     *     // pass the phantom record data to the server to be saved
     *     user.save({
     *         failure: function(record, operation) {
     *             // do something if the save failed
     *         },
     *         success: function(record, operation) {
     *             // do something if the save succeeded
     *         },
     *         callback: function(record, operation, success) {
     *             // do something whether the save succeeded or failed
     *         }
     *     });
     *
     * The response from a create operation should include the ID for the newly created
     * record:
     *
     *     // sample response
     *     {
     *         success: true,
     *         id: 1
     *     }
     *
     *     // the id may be nested if the proxy's reader has a rootProperty config
     *     Ext.define('MyApp.model.User', {
     *         extend: 'Ext.data.Model',
     *         proxy: {
     *             type: 'ajax',
     *             url: 'server.url',
     *             reader: {
     *                 type: 'ajax',
     *                 rootProperty: 'data'
     *             }
     *         }
     *     });
     *
     *     // sample nested response
     *     {
     *         success: true,
     *         data: {
     *             id: 1
     *         }
     *     }
     *
     * (Create + ) Update example:
     *
     *     Ext.define('MyApp.model.User', {
     *         extend: 'Ext.data.Model',
     *         fields: [
     *             {name: 'id', type: 'int'},
     *             {name: 'name', type: 'string'}
     *         ],
     *         proxy: {
     *             type: 'ajax',
     *             url: 'server.url'
     *         }
     *     });
     *
     *     var user = new MyApp.model.User({
     *         name: 'Foo'
     *     });
     *     user.save({
     *         success: function(record, operation) {
     *             record.set('name', 'Bar');
     *             // updates the remote record via the proxy
     *             record.save();
     *         }
     *     });
     *
     * (Create + ) Destroy example - see also {@link #erase}:
     *
     *     Ext.define('MyApp.model.User', {
     *         extend: 'Ext.data.Model',
     *         fields: [
     *             {name: 'id', type: 'int'},
     *             {name: 'name', type: 'string'}
     *         ],
     *         proxy: {
     *             type: 'ajax',
     *             url: 'server.url'
     *         }
     *     });
     *
     *     var user = new MyApp.model.User({
     *         name: 'Foo'
     *     });
     *     user.save({
     *         success: function(record, operation) {
     *             record.drop();
     *             // destroys the remote record via the proxy
     *             record.save();
     *         }
     *     });
     *
     * **NOTE:** If a {@link #phantom} record is {@link #drop dropped} and subsequently
     * saved it will not be processed via the proxy.  However, any passed `success`
     * or `callback` functions will be called.
     *
     * The options param is an Operation config object containing success, failure and
     * callback functions, plus optional scope.  The type of Operation depends on the
     * state of the model being saved.
     *
     *  - {@link #phantom} model - {@link Ext.data.operation.Create}
     *  - {@link #isModified modified} model - {@link Ext.data.operation.Update}
     *  - {@link #dropped} model - {@link Ext.data.operation.Destroy}
     *
     * @inheritdoc #method-load
     * @return {Ext.data.operation.Create/Ext.data.operation.Update/Ext.data.operation.Destroy}
     * The operation instance for saving this model.  The type of operation returned
     * depends on the model state at the time of the action.
     *
     *  - {@link #phantom} model - {@link Ext.data.operation.Create}
     *  - {@link #isModified modified} model - {@link Ext.data.operation.Update}
     *  - {@link #dropped} model - {@link Ext.data.operation.Destroy}
     */
    save: function(options) {
        options = Ext.apply({}, options);
        
        /* eslint-disable-next-line vars-on-top */
        var me = this,
            phantom = me.phantom,
            dropped = me.dropped