messengercustom-servicesmacoslinuxwindowsinboxwhatsappicloudtweetdeckhipchattelegramhangoutsslackgmailskypefacebook-workplaceoutlookemailmicrosoft-teamsdiscord
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
1123 lines
39 KiB
9 years ago
|
/**
|
||
|
* 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
|
||
|
}
|
||
|
}
|
||
|
});
|