Форк Rambox
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1123 lines
39 KiB

/**
* 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 `destory` operations to
* save all of the changes in the Session.
*
* @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'
],
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'
}
},
destroyed: false,
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
},
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.identifierCache = {};
// Bind ourselves so we're always called in our own scope.
me.recordCreator = me.recordCreator.bind(me);
me.initConfig(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 = record.session = null;
}
}
}
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.Error.raise('Record already belongs to an existing session');
}
//</debug>
if (record.session !== me) {
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 data = this.data,
entityName, entities, id, record;
for (entityName in data) {
entities = data[entityName];
for (id in entities) {
record = entities[id].record;
if (record) {
record.commit();
}
}
}
},
/**
* 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.
* @return {Ext.data.Model} The new record.
*/
createRecord: function (type, data) {
//<debug>
this.checkModelType(type);
//</debug>
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.Error.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, 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} type The `entityName` or the actual class of record to create.
* @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#load} call.
* @return {Ext.data.Model} The record.
*/
getRecord: function(type, id, autoLoad) {
var me = this,
record = me.peekRecord(type, id),
Model, parent, parentRec;
if (!record) {
Model = type.$isClass ? type : me.getSchema().getEntity(type);
parent = me.getParent();
if (parent) {
parentRec = parent.peekRecord(Model, id);
}
if (parentRec && !parentRec.isLoading()) {
record = parentRec.copy(undefined, me);
record.$source = parentRec;
} 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.Error.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.Error.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.Error.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.Error.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.Error.raise('Cannot update, record dropped: ' + this.getModelIdentifier(entityType, id));
} else {
Ext.Error.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>
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() {
//<debug>
if (!this.getParent()) {
Ext.Error.raise('Cannot commit session, no parent exists');
}
//</debug>
var visitor = new Ext.data.session.ChildChangesVisitor(this);
this.visitData(visitor);
this.getParent().update(visitor.result);
},
/**
* 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,
entityName, entityType, entityInfo, i,
operation, item, associations, key, role, associationData;
// 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.Error.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.Error.raise('Invalid association key for ' + entityName + ', "' + key + '"');
}
//</debug>
associationData = entityInfo[role.role];
role.processUpdate(me, associationData);
}
}
},
//-------------------------------------------------------------------------
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.Error.raise('Duplicate id ' + record.id + ' for ' + record.entityName);
}
//</debug>
entry.record = record;
me.registerReferences(record);
associations = record.associations;
for (roleName in associations) {
associations[roleName].checkMembership(me, record);
}
},
/**
* Template method, will be called by Model after a record is dropped.
* @param {Ext.data.Model} record The record.
*
* @private
*/
afterErase: function(record) {
this.evict(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.Error.raise('Unable to use anonymous models in a Session');
} else if (!this.getSchema().getEntity(name)) {
Ext.Error.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 len = items.length,
i, data, rec, id;
for (i = 0; i < len; ++i) {
data = items[i];
id = entityType.getIdFromData(data);
rec = this.peekRecord(entityType, id);
if (!rec) {
rec = this.createRecord(entityType, data);
} else {
this.onInvalidEntityCreate(entityType, id);
}
// This record has been marked as being created, so we must
// be a phantom
rec.phantom = true;
}
},
/**
* 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 entityName = record.entityName,
entities = this.data[entityName],
id = record.id,
entry;
if (entities) {
delete entities[id];
}
},
/**
* 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;
}
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.id || entityType.entityName;
ret = cache[key];
if (!ret) {
if (identifier.clone) {
ret = identifier.clone({
cache: cache
});
} 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,
entityName = record.entityName,
id = record.id,
bucket = me.data[entityName],
entry = bucket[oldId],
associations = record.associations,
refs = entry.refs,
setNoRefs = me._setNoRefs,
association, fieldName, matrix, refId, role, roleName, roleRefs, store;
//<debug>
if (bucket[newId]) {
Ext.Error.raise('Cannot change ' + entityName + ' id from ' + oldId +
' to ' + newId + ' id already exists');
}
//</debug>
delete bucket[oldId];
bucket[newId] = entry;
for (roleName in associations) {
role = associations[roleName];
if (role.isMany) {
store = role.getAssociatedItem(record);
if (store) {
matrix = store.matrix;
if (matrix) {
matrix.changeId(newId);
}
}
}
}
if (refs) {
for (roleName in refs) {
roleRefs = refs[roleName];
role = associations[roleName];
association = role.association;
if (association.isManyToMany) {
// TODO
} else {
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 len = items.length,
i, data, rec, id;
for (i = 0; i < len; ++i) {
data = items[i];
id = entityType.getIdFromData(data);
rec = this.peekRecord(entityType, id);
if (!rec) {
rec = this.createRecord(entityType, data);
} else {
this.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;
}
},
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 {
//TODO no easy answer here... we are trying to create a record and have
//TODO some (potentially new) data. We probably should check for mid-air
//TODO collisions using versionProperty but for now we just ignore the
//TODO new data in favor of our potentially edited data.
// Peek checks if it exists at any level, by getting it we ensure that the record is copied down
record = me.getRecord(Model, id);
}
return record;
},
registerReferences: function (record, oldId) {
var entityName = record.entityName,
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;
// 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
}
}
});