/** * This class manages models and their associations. Instances of `Session` are typically * associated with some `Component` (perhaps the Viewport or a Window) and then used by * their `{@link Ext.app.ViewModel view models}` to enable data binding. * * The primary job of a Session is to manage a collection of records of many different * types and their associations. This often starts by loading records when requested (via * bind - see below) and culminates when it is time to save to the server. * * Because the Session tracks all records it loads, it ensures that for any given type of * model, only one record exists with a given `id`. This means that all edits of that * record are properly targeted at that one instance. * * Similarly, when associations are loaded, the `Ext.data.Store` created to hold the * associated records is tracked by the Session. So all requests for the "OrderItems" of * a particular Order id will result in the same Store. Adding and removing items from * that Order then is sure to remain consistent. * * # Data * * Since the Session is managing all this data, there are several methods it provides * to give convenient access to that data. The most important of these is `update` and * `getChanges`. * * The `update` and `getChanges` methods both operate on object that contains a summary * of records and associations and different CRUD operations. * * ## Saving * * There are two basic ways to save the contents of a Session: `getChanges` and * `getSaveBatch`. We've already seen `getChanges`. The data contained in the CRUD object * can be translated into whatever shape is needed by the server. * * To leverage the `{@link Ext.data.Model#proxy proxy}` facilities defined by each Model * class, there is the `getSaveBatch` method. That method returns an `Ext.data.Batch` * object populated with the necessary `create`, `update` and `destroy` operations to * save all of the changes in the Session. * * ## Conflicts * * If data is loaded from the server (for example a store load) and there is an existing record, * the {@link Ext.data.Model#method-mergeData `mergeData`} method will be called to resolve * the conflict. * * @since 5.0.0 */Ext.define('Ext.data.Session', { requires: [ 'Ext.data.schema.Schema', 'Ext.data.Batch', 'Ext.data.matrix.Matrix', 'Ext.data.session.ChangesVisitor', 'Ext.data.session.ChildChangesVisitor', 'Ext.data.session.BatchVisitor' ], mixins: [ 'Ext.mixin.Dirty', 'Ext.mixin.Observable' ], isSession: true, config: { /** * @cfg {String/Ext.data.schema.Schema} schema */ schema: 'default', /** * @cfg {Ext.data.Session} parent * The parent session for this session. */ parent: null, /** * @cfg {Boolean} autoDestroy * `true` to automatically destroy this session when a component it is attached * to is destroyed. This should be set to false if the session is intended to be * used across multiple root level components. * * @since 5.0.1 */ autoDestroy: true, crudProperties: { create: 'C', read: 'R', update: 'U', drop: 'D' } }, crudOperations: [{ type: 'R', entityMethod: 'readEntities' }, { type: 'C', entityMethod: 'createEntities' }, { type: 'U', entityMethod: 'updateEntities' }, { type: 'D', entityMethod: 'dropEntities' }], crudKeys: { C: 1, R: 1, U: 1, D: 1 }, statics: { nextId: 1 }, constructor: function(config) { var me = this; /* * { * User: { * 1: { * record: user1Instance, * refs: { * posts: { * 101: post101Instance, * 102: post202Instance * } * } * } * } * } */ me.data = {}; /* * { * UserGroups: new Ext.data.matrix.Matrix({ * association: UserGroups * }) * } */ me.matrices = {}; me.id = Ext.data.Session.nextId++; me.identifierCache = {}; // Bind ourselves so we're always called in our own scope. me.recordCreator = me.recordCreator.bind(me); me.mixins.observable.constructor.call(me, config); }, destroy: function() { var me = this, matrices = me.matrices, data = me.data, entityName, entities, record, id; for (id in matrices) { matrices[id].destroy(); } for (entityName in data) { entities = data[entityName]; for (id in entities) { record = entities[id].record; if (record) { // Clear up any source if we pushed one on, remove // the session reference record.$source = null; // While we don't actually call join() for the session, we need to // tell the records that they are being detached from the session in // some way. record.unjoin(me); // see also evict() } } } me.identifierCache = me.recordCreator = me.matrices = me.data = null; me.setSchema(null); me.callParent(); }, /** * Adds an existing record instance to the session. The record * may not belong to another session. The record cannot be a phantom record, instead * use {@link #createRecord}. * @param {Ext.data.Model} record The record to adopt. */ adopt: function(record) { var me = this, associations = record.associations, roleName; //<debug> me.checkModelType(record.self); if (record.session && record.session !== me) { Ext.raise('Record already belongs to an existing session'); } //</debug> if (record.session !== me) { me.add(record); if (associations) { for (roleName in associations) { associations[roleName].adoptAssociated(record, me); } } } }, /** * Marks the session as "clean" by calling {@link Ext.data.Model#commit} on each record * that is known to the session. * * - Phantom records will no longer be phantom. * - Modified records will no longer be dirty. * - Dropped records will be erased. * * @since 5.1.0 */ commit: function() { var me = this, data = me.data, matrices = me.matrices, dirtyWas = me.getDirty(), entityName, entities, id, record; me.suspendEvent('dirtychange'); for (entityName in data) { entities = data[entityName]; for (id in entities) { record = entities[id].record; if (record) { record.commit(); } } } for (id in matrices) { matrices[id].commit(); } me.clearRecordStates(); me.resumeEvent('dirtychange'); if (me.getDirty() !== dirtyWas) { me.fireDirtyChange(); } }, /** * Creates a new record and tracks it in this session. * * @param {String/Ext.Class} type The `entityName` or the actual class of record to create. * @param {Object} [data] The data for the record. * @param {Boolean} [preventAdd] (private) `true` to prevent the record from being added * to the session * @return {Ext.data.Model} The new record. */ createRecord: function(type, data, preventAdd) { //<debug> this.checkModelType(type); //</debug> /* eslint-disable-next-line vars-on-top */ var Model = type.$isClass ? type : this.getSchema().getEntity(type), parent = this.getParent(), id; // If we have no data, we're creating a phantom if (data && parent) { id = Model.getIdFromData(data); if (parent.peekRecord(Model, id)) { Ext.raise('A parent session already contains an entry for ' + Model.entityName + ': ' + id); } } // By passing the session to the constructor, it will call session.add() return new Model(data, preventAdd ? null : this); }, /** * Returns an object describing all of the modified fields, created or dropped records * and many-to-many association changes maintained by this session. * * @return {Object} An object in the CRUD format (see the intro docs). `null` if there are * no changes. */ getChanges: function() { var visitor = new Ext.data.session.ChangesVisitor(this); this.visitData(visitor); return visitor.result; }, /** * The same functionality as {@link #getChanges}, however we also take into account our * parent session. * * @return {Object} An object in the CRUD format (see the intro docs). `null` if there are * no changes. * * @protected */ getChangesForParent: function() { var visitor = new Ext.data.session.ChildChangesVisitor(this); this.visitData(visitor); return visitor.result; }, /** * Get a cached record from the session. If the record does not exist, it will * be created. If the `autoLoad` parameter is not set to `false`, the record will * be loaded via the {@link Ext.data.Model#proxy proxy} of the Model. * * If this session is configured with a `{@link #parent}` session, a *copy* of any existing * record in the `parent` will be adopted into this session. If the `parent` does not contain * the record, the record will be created and *not* inserted into the parent. * * See also {@link #peekRecord}. * * @param {String/Ext.Class/Ext.data.Model} type The `entityName` or the actual class of record * to create. This may also be a record instance, where the type and id will be inferred from * the record. If the record is not attached to a session, it will be adopted. If it exists * in a parent, an appropriate copy will be made as described. * @param {Object} id The id of the record. * @param {Boolean/Object} [autoLoad=true] `false` to prevent the record from being loaded if * it does not exist. If this parameter is an object, it will be passed to the * {@link Ext.data.Model#method!load} call. * @return {Ext.data.Model} The record. */ getRecord: function(type, id, autoLoad) { var me = this, wasInstance = type.isModel, record, Model, parent, parentRec; if (wasInstance) { wasInstance = type; id = type.id; type = type.self; } record = me.peekRecord(type, id); if (!record) { Model = type.$isClass ? type : me.getSchema().getEntity(type); parent = me.getParent(); if (parent) { parentRec = parent.peekRecord(Model, id); } if (parentRec) { if (parentRec.isLoading()) { // If the parent is loading, it's as though it doesn't have // the record, so we can't copy it, but we don't want to // adopt it either. wasInstance = false; } else { record = parentRec.copy(undefined, me); record.$source = parentRec; } } if (!record) { if (wasInstance) { record = wasInstance; me.adopt(record); } else { record = Model.createWithId(id, null, me); if (autoLoad !== false) { record.load(Ext.isObject(autoLoad) ? autoLoad : undefined); } } } } return record; }, /** * Returns an `Ext.data.Batch` containing the `Ext.data.operation.Operation` instances * that are needed to save all of the changes in this session. This sorting is based * on operation type, associations and foreign keys. Generally speaking the operations * in the batch can be committed to a server sequentially and the server will never be * sent a request with an invalid (client-generated) id in a foreign key field. * * @param {Boolean} [sort=true] Pass `false` to disable the batch operation sort. * @return {Ext.data.Batch} */ getSaveBatch: function(sort) { var visitor = new Ext.data.session.BatchVisitor(); this.visitData(visitor); return visitor.getBatch(sort); }, /** * Triggered when an associated item from {@link #update} references a record * that does not exist in the session. * @param {Ext.Class} entityType The entity type. * @param {Object} id The id of the model. * * @protected * @template */ onInvalidAssociationEntity: function(entityType, id) { Ext.raise('Unable to read association entity: ' + this.getModelIdentifier(entityType, id)); }, /** * Triggered when an drop block from {@link #update} tries to create a record * that already exists. * @param {Ext.Class} entityType The entity type. * @param {Object} id The id of the model. * * @protected * @template */ onInvalidEntityCreate: function(entityType, id) { Ext.raise('Cannot create, record already not exists: ' + this.getModelIdentifier(entityType, id)); }, /** * Triggered when an drop block from {@link #update} references a record * that does not exist in the session. * @param {Ext.Class} entityType The entity type. * @param {Object} id The id of the model. * * @protected * @template */ onInvalidEntityDrop: function(entityType, id) { Ext.raise('Cannot drop, record does not exist: ' + this.getModelIdentifier(entityType, id)); }, /** * Triggered when an drop block from {@link #update} tries to create a record * that already exists. * @param {Ext.Class} entityType The entity type. * @param {Object} id The id of the model. * * @protected * @template */ onInvalidEntityRead: function(entityType, id) { Ext.raise('Cannot read, record already not exists: ' + this.getModelIdentifier(entityType, id)); }, /** * Triggered when an update block from {@link #update} references a record * that does not exist in the session. * @param {Ext.Class} entityType The entity type. * @param {Object} id The id of the model. * @param {Boolean} dropped `true` if the record was dropped. * * @protected * @template */ onInvalidEntityUpdate: function(entityType, id, dropped) { if (dropped) { Ext.raise('Cannot update, record dropped: ' + this.getModelIdentifier(entityType, id)); } else { Ext.raise('Cannot update, record does not exist: ' + this.getModelIdentifier(entityType, id)); } }, /** * Gets an existing record from the session. The record will *not* be created if it does * not exist. * * See also: {@link #getRecord}. * * @param {String/Ext.Class} type The `entityName` or the actual class of record to create. * @param {Object} id The id of the record. * @param {Boolean} [deep=false] `true` to consult * @return {Ext.data.Model} The record, `null` if it does not exist. */ peekRecord: function(type, id, deep) { // Duplicate some of this logic from getEntry here to prevent the creation // of entries when asking for the existence of records. We may not need them //<debug> this.checkModelType(type); //</debug> /* eslint-disable-next-line vars-on-top */ var entityType = type.$isClass ? type : this.getSchema().getEntity(type), entityName = entityType.entityName, entry = this.data[entityName], ret, parent; entry = entry && entry[id]; ret = entry && entry.record; if (!ret && deep) { parent = this.getParent(); ret = parent && parent.peekRecord(type, id, deep); } return ret || null; }, /** * Save any changes in this session to a {@link #parent} session. */ save: function() { var me = this, parent = me.getParent(), visitor; if (parent) { visitor = new Ext.data.session.ChildChangesVisitor(me); me.visitData(visitor); parent.update(visitor.result); me.commit(); } //<debug> else { Ext.raise('Cannot commit session, no parent exists'); } //</debug> }, /** * Create a child session with this session as the {@link #parent}. * @return {Ext.data.Session} The copied session. */ spawn: function() { return new this.self({ schema: this.getSchema(), parent: this }); }, /** * Complete a bulk update for this session. * @param {Object} data Data in the CRUD format (see the intro docs). */ update: function(data) { var me = this, schema = me.getSchema(), crudOperations = me.crudOperations, len = crudOperations.length, crudKeys = me.crudKeys, dirtyWas = me.getDirty(), entityName, entityType, entityInfo, i, operation, item, associations, key, role, associationData; me.suspendEvent('dirtychange'); // Force the schema to process any pending drops me.getSchema().processKeyChecks(true); // Do a first pass to setup all the entities first for (entityName in data) { entityType = schema.getEntity(entityName); //<debug> if (!entityType) { Ext.raise('Invalid entity type: ' + entityName); } //</debug> entityInfo = data[entityName]; for (i = 0; i < len; ++i) { operation = crudOperations[i]; item = entityInfo[operation.type]; if (item) { me[operation.entityMethod](entityType, item); } } } // A second pass to process associations once we have all the entities in place for (entityName in data) { entityType = schema.getEntity(entityName); associations = entityType.associations; entityInfo = data[entityName]; for (key in entityInfo) { // Skip over CRUD, just looking for associations here if (crudKeys[key]) { continue; } role = associations[key]; //<debug> if (!role) { Ext.raise('Invalid association key for ' + entityName + ', "' + key + '"'); } //</debug> associationData = entityInfo[role.role]; role.processUpdate(me, associationData); } } me.resumeEvent('dirtychange'); if (me.getDirty() !== dirtyWas) { me.fireDirtyChange(); } }, //------------------------------------------------------------------------- /** * Template method, will be called by Model after a record is committed. * @param {Ext.data.Model} record The record. * * @protected * @since 6.2.0 */ afterCommit: function(record) { this.trackRecordState(record); }, /** * Template method, will be called by Model after a record is dropped. * @param {Ext.data.Model} record The record. * * @protected * @since 6.2.0 */ afterDrop: function(record) { this.trackRecordState(record); }, /** * Template method, will be called by Model after a record is edited. * @param {Ext.data.Model} record The record. * * @protected * @since 6.2.0 */ afterEdit: function(record) { this.trackRecordState(record); }, /** * Template method, will be called by Model after a record is erased (a drop * that is committed). * @param {Ext.data.Model} record The record. * * @protected */ afterErase: function(record) { this.evict(record); }, /** * Template method, will be called by Model after a record is rejected. * @param {Ext.data.Model} record The record. * * @protected * @since 6.5.1 */ afterReject: function(record) { this.trackRecordState(record); }, privates: { /** * Add a record instance to this session. Called by model. * @param {Ext.data.Model} record The record. * * @private */ add: function(record) { var me = this, id = record.id, entry = me.getEntry(record.self, id), associations, roleName; //<debug> if (entry.record) { Ext.raise('Duplicate id ' + record.id + ' for ' + record.entityName); } //</debug> record.session = me; entry.record = record; me.trackRecordState(record, true); me.registerReferences(record); associations = record.associations; for (roleName in associations) { associations[roleName].checkMembership(me, record); } }, /** * @private */ applySchema: function(schema) { return Ext.data.schema.Schema.get(schema); }, //<debug> /** * Checks if the model type being referenced is valid for this session. That includes * checking if the model name is correct & is one used in this {@link #schema} for this * session. Will raise an exception if the model type is not correct. * @param {String/Ext.Class} name The model name or model type. * * @private */ checkModelType: function(name) { if (name.$isClass) { name = name.entityName; } if (!name) { Ext.raise('Unable to use anonymous models in a Session'); } else if (!this.getSchema().getEntity(name)) { Ext.raise('Unknown entity type ' + name); } }, //</debug> /** * Process a create block of entities from the {@link #update} method. * @param {Ext.Class} entityType The entity type. * @param {Object[]} items The data objects to create. * * @private */ createEntities: function(entityType, items) { var me = this, len = items.length, i, data, rec, id; for (i = 0; i < len; ++i) { data = items[i]; id = entityType.getIdFromData(data); rec = me.peekRecord(entityType, id); if (!rec) { // Wait until after creating the record before adding it to the session, // instead of allowing the Model constructor to call session.add(). // This allows us to first initialize the phantom and crudState properties. // so that the session sets its dirty state correctly when add() is called. // The Model constructor usually handles setting phantom/crudState, // but in this case it will not detect the record as phantom because // we are passing an id (generated by the child session) to the Model // constructor. rec = me.createRecord(entityType, data, true); rec.phantom = true; rec.crudState = 'C'; me.add(rec); // Be sure to set this after "notifying" the session. rec.crudStateWas = 'C'; } else { me.onInvalidEntityCreate(entityType, id); } } }, /** * Process a drop block for entities from the {@link #update} method. * @param {Ext.Class} entityType The entity type. * @param {Object[]} ids The identifiers of the items to drop. * * @private */ dropEntities: function(entityType, ids) { var len = ids.length, i, rec, id, extractId; if (len) { // Handle writeAllFields here, we may not have an array of raw ids extractId = Ext.isObject(ids[0]); } for (i = 0; i < len; ++i) { id = ids[i]; if (extractId) { id = entityType.getIdFromData(id); } rec = this.peekRecord(entityType, id); if (rec) { rec.drop(); } else { this.onInvalidEntityDrop(entityType, id); } } }, /** * Remove a record and any references from the session. * @param {Ext.data.Model} record The record * * @private */ evict: function(record) { var me = this, entityName = record.entityName, entities = me.data[entityName], id = record.id; if (entities && entities[id]) { me.untrackRecordState(record); // While we don't actually call join() for the session, we need to // tell the records that they are being detached from the session in // some way. record.unjoin(me); delete entities[id]; // see also destroy() } }, /** * Transforms a list of ids into a list of records for a particular type. * @param {Ext.Class} entityType The entity type. * @param {Object[]} ids The ids to transform. * @return {Ext.data.Model[]} The models corresponding to the ids. */ getEntityList: function(entityType, ids) { var len = ids.length, i, id, rec, invalid; for (i = 0; i < len; ++i) { id = ids[i]; rec = this.peekRecord(entityType, id); if (rec) { ids[i] = rec; } else { invalid = true; ids[i] = null; this.onInvalidAssociationEntity(entityType, id); } } if (invalid) { ids = Ext.Array.clean(ids); } return ids; }, /** * Return an entry for the data property for a particular type/id. * @param {String/Ext.Class} type The entity name or model type. * @param {Object} id The id of the record * @return {Object} The data entry. * * @private */ getEntry: function(type, id) { if (type.isModel) { id = type.getId(); type = type.self; } /* eslint-disable-next-line vars-on-top */ var entityType = type.$isClass ? type : this.getSchema().getEntity(type), entityName = entityType.entityName, data = this.data, entry; entry = data[entityName] || (data[entityName] = {}); entry = entry[id] || (entry[id] = {}); return entry; }, getRefs: function(record, role, includeParent) { var entry = this.getEntry(record), refs = entry && entry.refs && entry.refs[role.role], parent = includeParent && this.getParent(), parentRefs, id, rec; if (parent) { parentRefs = parent.getRefs(record, role); if (parentRefs) { for (id in parentRefs) { rec = parentRefs[id]; if ((!refs || !refs[id])) { // We don't know about this record but the parent does. We need to // pull it down so it may be edited as part of the collection this.getRecord(rec.self, rec.id); } } // Recalculate our refs after we pull down all the required records refs = entry && entry.refs && entry.refs[role.role]; } } return refs || null; }, getIdentifier: function(entityType) { var parent = this.getParent(), cache, identifier, key, ret; if (parent) { ret = parent.getIdentifier(entityType); } else { cache = this.identifierCache; identifier = entityType.identifier; key = identifier.getId() || entityType.entityName; ret = cache[key]; if (!ret) { if (identifier.clone) { ret = identifier.clone({ id: null }); } else { ret = identifier; } cache[key] = ret; } } return ret; }, getMatrix: function(matrix, preventCreate) { var name = matrix.isManyToMany ? matrix.name : matrix, matrices = this.matrices, ret; ret = matrices[name]; if (!ret && !preventCreate) { ret = matrices[name] = new Ext.data.matrix.Matrix(this, matrix); } return ret || null; }, getMatrixSlice: function(role, id) { var matrix = this.getMatrix(role.association), side = matrix[role.side]; return side.get(id); }, /** * Gets a user friendly identifier for a Model. * @param {Ext.Class} entityType The entity type. * @param {Object} id The id of the entity. * @return {String} The identifier. */ getModelIdentifier: function(entityType, id) { return id + '@' + entityType.entityName; }, onIdChanged: function(record, oldId, newId) { var me = this, matrices = me.matrices, entityName = record.entityName, id = record.id, bucket = me.data[entityName], entry = bucket[oldId], associations = record.associations, refs = entry.refs, setNoRefs = me._setNoRefs, association, fieldName, refId, role, roleName, roleRefs, key; //<debug> if (bucket[newId]) { Ext.raise('Cannot change ' + entityName + ' id from ' + oldId + ' to ' + newId + ' id already exists'); } //</debug> delete bucket[oldId]; bucket[newId] = entry; for (key in matrices) { matrices[key].updateId(record, oldId, newId); } if (refs) { for (roleName in refs) { roleRefs = refs[roleName]; role = associations[roleName]; association = role.association; if (!association.isManyToMany) { fieldName = association.field.name; for (refId in roleRefs) { roleRefs[refId].set(fieldName, id, setNoRefs); } } } } me.registerReferences(record, oldId); }, processManyBlock: function(entityType, role, items, processor) { var me = this, id, record, records, store; if (items) { for (id in items) { record = me.peekRecord(entityType, id); if (record) { records = me.getEntityList(role.cls, items[id]); store = role.getAssociatedItem(record); me[processor](role, store, record, records); } else { me.onInvalidAssociationEntity(entityType, id); } } } }, processManyCreate: function(role, store, record, records) { if (store) { // Will handle any duplicates store.add(records); } else { record[role.getterName](null, null, records); } }, processManyDrop: function(role, store, record, records) { if (store) { store.remove(records); } }, processManyRead: function(role, store, record, records) { if (store) { store.setRecords(records); } else { // We don't have a store. Create it and add the records. record[role.getterName](null, null, records); } }, /** * Process a read block of entities from the {@link #update} method. * @param {Ext.Class} entityType The entity type. * @param {Object[]} items The data objects to read. * * @private */ readEntities: function(entityType, items) { var me = this, len = items.length, i, data, rec, id; for (i = 0; i < len; ++i) { data = items[i]; id = entityType.getIdFromData(data); rec = me.peekRecord(entityType, id); if (!rec) { rec = me.createRecord(entityType, data, true); } else { me.onInvalidEntityRead(entityType, id); } // We've been read from a "server", so we aren't a phantom, // regardless of whether or not we have an id rec.phantom = false; me.add(rec); } }, recordCreator: function(data, Model) { var me = this, id = Model.getIdFromData(data), record = me.peekRecord(Model, id, true); // It doesn't exist anywhere, create it if (!record) { // We may have a stub that is loading the record (in fact this may be the // call coming from that Reader), but the resolution is simple. By creating // the record it is registered in the data[entityName][id] entry anyway // and the stub will deal with it onLoad. record = new Model(data, me); } else { record = me.getRecord(Model, id); record.mergeData(data); } return record; }, registerReferences: function(record, oldId) { var entityName = record.entityName, // eslint-disable-line no-unused-vars id = record.id, recordData = record.data, remove = oldId || oldId === 0, entry, i, fk, len, reference, references, refs, roleName; // Register this records references to other records len = (references = record.references).length; for (i = 0; i < len; ++i) { reference = references[i]; // e.g., an orderId field fk = recordData[reference.name]; // the orderId if (fk || fk === 0) { reference = reference.reference; // the "order" association role entityName = reference.type; roleName = reference.inverse.role; // Track down the entry for the associated record entry = this.getEntry(reference.cls, fk); refs = entry.refs || (entry.refs = {}); refs = refs[roleName] || (refs[roleName] = {}); refs[id] = record; if (remove) { delete refs[oldId]; } } } }, /** * Process an update block for entities from the {@link #update} method. * @param {Ext.Class} entityType The entity type. * @param {Object[]} items The data objects to update. * * @private */ updateEntities: function(entityType, items) { var len = items.length, i, data, rec, id, modified; // eslint-disable-line no-unused-vars // Repeating some code here, but we want to optimize this for speed if (Ext.isArray(items)) { for (i = 0; i < len; ++i) { data = items[i]; id = entityType.getIdFromData(data); rec = this.peekRecord(entityType, id); if (rec) { rec.set(data); } else { this.onInvalidEntityUpdate(entityType, id); } } } else { for (id in items) { data = items[id]; rec = this.peekRecord(entityType, id); if (rec && !rec.dropped) { modified = rec.set(data); } else { this.onInvalidEntityUpdate(entityType, id, !!rec); } } } }, updateReference: function(record, field, newValue, oldValue) { var reference = field.reference, entityName = reference.type, roleName = reference.inverse.role, id = record.id, entry, refs; if (oldValue || oldValue === 0) { // We must be already in this entry.refs collection refs = this.getEntry(entityName, oldValue).refs[roleName]; delete refs[id]; } if (newValue || newValue === 0) { entry = this.getEntry(entityName, newValue); refs = entry.refs || (entry.refs = {}); refs = refs[roleName] || (refs[roleName] = {}); refs[id] = record; } }, /** * Walks the internal data tracked by this session and calls methods on the provided * `visitor` object. The visitor can then accumulate whatever data it finds important. * The visitor object can provide a number of methods, but all are optional. * * This method does not enumerate associations since these can be traversed given the * records that are enumerated. For many-to-many associations, however, this method * does enumerate the changes because these changes are not "owned" by either side of * such associations. * * @param {Object} visitor * @param {Function} [visitor.onCleanRecord] This method is called to describe a record * that is known but unchanged. * @param {Ext.data.Model} visitor.onCleanRecord.record The unmodified record. * @param {Function} [visitor.onDirtyRecord] This method is called to describe a record * that has either been created, dropped or modified. * @param {Ext.data.Model} visitor.onDirtyRecord.record The modified record. * @param {Function} [visitor.onMatrixChange] This method is called to describe a * change in a many-to-many association (a "matrix"). * @param {Ext.data.schema.Association} visitor.onMatrixChange.association The object * describing the many-to-many ("matrix") association. * @param {Mixed} visitor.onMatrixChange.leftId The `idProperty` of the record on the * "left" of the association. * @param {Mixed} visitor.onMatrixChange.rightId The `idProperty` of the record on the * "right" of the association. * @param {Number} visitor.onMatrixChange.state A negative number if the two records * are being disassociated or a positive number if they are being associated. For * example, when adding User 10 to Group 20, this would be 1. When removing the User * this argument would be -1. * @return {Object} The visitor instance */ visitData: function(visitor) { var me = this, data = me.data, matrices = me.matrices, all, assoc, id, id2, matrix, members, name, record, slice, slices, state; // Force the schema to process any pending drops me.getSchema().processKeyChecks(true); for (name in data) { all = data[name]; // all entities of type "name" for (id in all) { record = all[id].record; if (record) { if (record.phantom || record.dirty || record.dropped) { if (visitor.onDirtyRecord) { visitor.onDirtyRecord(record); } } else if (visitor.onCleanRecord) { visitor.onCleanRecord(record); } } } } if (visitor.onMatrixChange) { for (name in matrices) { matrix = matrices[name].left; // e.g., UserGroups.left (Users) slices = matrix.slices; assoc = matrix.role.association; for (id in slices) { slice = slices[id]; members = slice.members; for (id2 in members) { state = (record = members[id2])[2]; if (state) { visitor.onMatrixChange(assoc, record[0], record[1], state); } } } } } return visitor; }, //--------------------------------------------------------------------- // Record callbacks called because we are the "session" for the record. _setNoRefs: { refs: false } }});