/**
* This relationship describes the case where any one entity of one type may relate to any
* number of entities of another type, and also in the reverse.
*
* This form of association cannot store id's in the related entities since that would
* limit the number of related entities to one for the entity with the foreign key. Instead,
* these relationships are typically implemented using a so-called "matrix" table. This
* table typically has two columns to hold the id's of a pair of related entities. This
* pair of id's is unique in the matrix table.
*
* # Declaration Forms
*
* // Fully spelled out - all properties are their defaults:
*
* Ext.define('App.models.Group', {
* extend: 'Ext.data.Model',
*
* manyToMany: {
* UserGroups: {
* type: 'User',
* role: 'users',
* field: 'userId',
* right: {
* field: 'groupId',
* role: 'groups'
* }
* }
* }
* });
*
* // Eliminate "right" object and use boolean to indicate Group is on the
* // right. By default, left/right is determined by alphabetic order.
*
* Ext.define('App.models.Group', {
* extend: 'Ext.data.Model',
*
* manyToMany: {
* UserGroups: {
* type: 'User',
* role: 'users',
* field: 'userId',
* right: true
* }
* }
* });
*
* // Eliminate object completely and rely on string to name the other type. Still
* // keep Group on the "right".
*
* Ext.define('App.models.Group', {
* extend: 'Ext.data.Model',
*
* manyToMany: {
* UserGroups: 'User#' // '#' is on the side (left or right) of Group
* }
* });
*
* // Remove explicit matrix name and keep Group on the "right". Generated matrixName
* // remains "UserGroups".
*
* Ext.define('App.models.Group', {
* extend: 'Ext.data.Model',
*
* manyToMany: [
* 'User#'
* ]
* });
*
* // Minimal definition but now Group is on the "left" since "Group" sorts before
* // "User". Generated matrixName is now "GroupUsers".
*
* Ext.define('App.models.Group', {
* extend: 'Ext.data.Model',
*
* manyToMany: [
* 'User'
* ]
* });
*/
Ext.define('Ext.data.schema.ManyToMany', {
extend: 'Ext.data.schema.Association',
isManyToMany: true,
isToMany: true,
kind: 'many-to-many',
Left: Ext.define(null, {
extend: 'Ext.data.schema.Role',
isMany: true,
digitRe: /^\d+$/,
findRecords: function(session, rightRecord, leftRecords) {
var slice = session.getMatrixSlice(this.inverse, rightRecord.id),
members = slice.members,
ret = [],
cls = this.cls,
seen, i, len, id, member, leftRecord;
if (leftRecords) {
seen = {};
// Loop over the records returned by the server and
// check they all still belong
for (i = 0, len = leftRecords.length; i < len; ++i) {
leftRecord = leftRecords[i];
id = leftRecord.id;
member = members[id];
if (!(member && member[2] === -1)) {
ret.push(leftRecord);
}
seen[id] = true;
}
}
// Loop over the expected set and include any missing records.
for (id in members) {
member = members[id];
if (!seen || !seen[id] && (member && member[2] !== -1)) {
leftRecord = session.peekRecord(cls, id);
if (leftRecord) {
ret.push(leftRecord);
}
}
}
return ret;
},
onIdChanged: function(rightRecord, oldId, newId) {
var store = this.getAssociatedItem(rightRecord);
if (store) {
store.getFilters().get(this.$roleFilterId).setValue(newId);
}
},
processLoad: function(store, rightRecord, leftRecords, session) {
var ret = leftRecords;
if (session) {
ret = this.findRecords(session, rightRecord, leftRecords);
this.onAddToMany(store, ret, true);
}
return ret;
},
processUpdate: function(session, associationData) {
var me = this,
entityType = me.inverse.cls,
items = associationData.R,
id, rightRecord, store, leftRecords;
if (items) {
for (id in items) {
rightRecord = session.peekRecord(entityType, id);
if (rightRecord) {
leftRecords = session.getEntityList(me.cls, items[id]);
store = me.getAssociatedItem(rightRecord);
if (store) {
store.loadData(leftRecords);
store.complete = true;
}
else {
// We don't have a store. Create it and add the records.
rightRecord[me.getterName](null, null, leftRecords);
}
}
else {
session.onInvalidAssociationEntity(entityType, id);
}
}
}
me.processMatrixBlock(session, associationData.C, 1);
me.processMatrixBlock(session, associationData.D, -1);
},
checkMembership: function(session, rightRecord) {
var matrix = session.getMatrix(this.association, true),
side, entityType, inverse, slice, slices,
id, members, member, leftRecord, store;
if (!matrix) {
return;
}
side = this.left ? matrix.right : matrix.left;
entityType = side.inverse.role.cls;
inverse = this.inverse;
slices = side.slices;
if (slices) {
slice = slices[rightRecord.id];
if (slice) {
members = slice.members;
for (id in members) {
member = members[id];
if (member[2] !== -1) {
// Do we have the record in the session?
// If so, do we also have the store?
leftRecord = session.peekRecord(entityType, id);
if (leftRecord) {
store = inverse.getAssociatedItem(leftRecord);
if (store) {
store.matrixUpdate = 1;
store.add(rightRecord);
store.matrixUpdate = 0;
}
}
}
}
}
}
},
onStoreCreate: function(store, session, id) {
var me = this,
matrix;
if (session) {
// If we are creating a store of say Groups in a UserGroups matrix, we want
// to traverse the inverse side of the matrix (Users) because the id we have
// is that of the User to which these Groups are associated.
matrix = session.getMatrixSlice(me.inverse, id);
matrix.attach(store);
matrix.notify = me.onMatrixUpdate;
matrix.scope = me;
}
},
processMatrixBlock: function(session, leftKeys, state) {
var inverse = this.inverse,
digitRe = this.digitRe,
slice, id;
if (leftKeys) {
for (id in leftKeys) {
// We may not have the record available to pull out the id, so the best we can
// do here is try to detect a number id.
if (digitRe.test(id)) {
id = parseInt(id, 10);
}
slice = session.getMatrixSlice(inverse, id);
slice.update(leftKeys[id], state);
}
}
},
createGetter: function() {
var me = this;
return function(options, scope, leftRecords) {
// 'this' refers to the Model instance inside this function
return me.getAssociatedStore(this, options, scope, leftRecords, false);
};
},
/*
* This method is called when records are added to the association store. If this
* is happening as a side-effect of the underlying matrix update, we skip telling
* the matrix what it already knows. Otherwise we need to tell the matrix of the
* changes on this side so that they can be reflected on the other side.
*/
onAddToMany: function(store, leftRecords, load) {
if (!store.matrixUpdate) {
store.matrixUpdate = 1;
// By default the "load" param is really the index, but we call this manually
// in a few spots to indicate it's a default load
store.matrix.update(leftRecords, load === true ? 0 : 1);
store.matrixUpdate = 0;
}
},
/*
* This method is called when records are removed from the association store. The
* same logic applies here as in onAddToMany with respect to the update that may
* or may not be taking place on the underlying matrix.
*/
onRemoveFromMany: function(store, records) {
if (!store.matrixUpdate) {
store.matrixUpdate = 1;
store.matrix.update(records, -1);
store.matrixUpdate = 0;
}
},
read: function(rightRecord, node, fromReader, readOptions) {
var me = this,
leftRecords = me.callParent([rightRecord, node, fromReader, readOptions]);
if (leftRecords) {
// Create the store and dump the data
rightRecord[me.getterName](null, null, leftRecords);
// Inline associations should *not* arrive on the "data" object:
delete rightRecord.data[me.role];
}
},
onMatrixUpdate: function(matrixSlice, id, state) {
var store = matrixSlice.store,
index, leftRecord, entry;
if (store && !store.loading && !store.matrixUpdate) {
store.matrixUpdate = 1;
index = store.indexOfId(id);
if (state < 0) {
if (index >= 0) {
store.remove([ index ]);
}
}
else if (index < 0) {
entry = store.getSession().getEntry(this.type, id);
leftRecord = entry && entry.record;
if (leftRecord) {
store.add(leftRecord);
}
}
store.matrixUpdate = 0;
}
},
adoptAssociated: function(record, session) {
var store = this.getAssociatedItem(record),
records, i, len;
if (store) {
store.setSession(session);
this.onStoreCreate(store, session, record.getId());
records = store.getData().items;
for (i = 0, len = records.length; i < len; ++i) {
session.adopt(records[i]);
}
}
}
}, function() {
var Left = this; // Left is created but ManyToMany may not yet be created
Ext.ClassManager.onCreated(function() {
Ext.data.schema.ManyToMany.prototype.Right = Ext.define(null, {
extend: Left,
left: false,
side: 'right'
});
}, null, 'Ext.data.schema.ManyToMany');
})
});