/**
* 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
}
}
});