/**
* **This class is never created directly. It should be constructed through associations
* in `Ext.data.Model`.**
*
* Declares a relationship between a single entity type and multiple related entities.
* The relationship can be declared as a keyed or keyless relationship.
*
* // Keyed
* Ext.define('Customer', {
* extend: 'Ext.data.Model',
* fields: ['id', 'name']
* });
*
* Ext.define('Ticket', {
* extend: 'Ext.data.Model',
* fields: ['id', 'title', {
* name: 'customerId',
* reference: 'Customer'
* }]
* });
*
* // Keyless
* Ext.define('Customer', {
* extend: 'Ext.data.Model',
* fields: ['id', 'name'],
* hasMany: 'Ticket'
* });
*
* Ext.define('Ticket', {
* extend: 'Ext.data.Model',
* fields: ['id', 'title']
* });
*
* // Generated methods
* var customer = new Customer();
* customer.tickets();
*
* var ticket = new Ticket();
* ticket.getCustomer();
* ticket.setCustomer();
*
* By declaring a keyed relationship, extra functionality is gained that maintains
* the key field in the model as changes are made to the association.
*
* For available configuration options, see {@link Ext.data.schema.Reference}.
* The "one" record type will have a generated {@link Ext.data.schema.Association#storeGetter}.
* The "many" record type will have a {@link Ext.data.schema.Association#recordGetter getter}
* and {@link Ext.data.schema.Association#recordSetter setter}.
*/
Ext.define('Ext.data.schema.ManyToOne', {
extend: 'Ext.data.schema.Association',
isManyToOne: true,
isToOne: true,
kind: 'many-to-one',
Left: Ext.define(null, {
extend: 'Ext.data.schema.Role',
isMany: true,
onDrop: function(rightRecord, session) {
var me = this,
store = me.getAssociatedItem(rightRecord),
leftRecords, len, i, id;
if (store) {
// Removing will cause the foreign key to be set to null.
leftRecords = store.removeAll();
if (leftRecords && me.inverse.owner) {
// If we're a child, we need to destroy all the "tickets"
for (i = 0, len = leftRecords.length; i < len; ++i) {
leftRecords[i].drop();
}
}
store.destroy();
rightRecord[me.getStoreName()] = null;
}
else if (session) {
leftRecords = session.getRefs(rightRecord, me);
if (leftRecords) {
for (id in leftRecords) {
leftRecords[id].drop();
}
}
}
},
onIdChanged: function(rightRecord, oldId, newId) {
var fieldName = this.association.getFieldName(),
store = this.getAssociatedItem(rightRecord),
leftRecords, i, len, filter;
if (store) {
filter = store.getFilters().get(this.$roleFilterId);
if (filter) {
filter.setValue(newId);
}
// A session will automatically handle this updating. If we don't have a field
// then there's nothing to do here.
if (!rightRecord.session && fieldName) {
leftRecords = store.getDataSource().items;
for (i = 0, len = leftRecords.length; i < len; ++i) {
leftRecords[i].set(fieldName, newId);
}
}
}
},
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);
}
}
}
},
findRecords: function(session, rightRecord, leftRecords, allowInfer) {
var ret = leftRecords,
refs = session.getRefs(rightRecord, this, true),
field = this.association.field,
fieldName, leftRecord, id, i, len, seen;
if (field && (refs || allowInfer)) {
fieldName = field.name;
ret = [];
if (leftRecords) {
seen = {};
// Loop over the records returned by the server and
// check they all still belong. If the session doesn't have any prior knowledge
// and we're allowed to infer the parent id (via nested loading), only do so if
// we explicitly have an id specified
for (i = 0, len = leftRecords.length; i < len; ++i) {
leftRecord = leftRecords[i];
id = leftRecord.id;
if (refs && refs[id]) {
ret.push(leftRecord);
}
else if (allowInfer && leftRecord.data[fieldName] === undefined) {
ret.push(leftRecord);
leftRecord.data[fieldName] = rightRecord.id;
session.updateReference(leftRecord, field, rightRecord.id, undefined);
}
seen[id] = true;
}
}
// Loop over the expected set and include any missing records.
if (refs) {
for (id in refs) {
if (!seen || !seen[id]) {
ret.push(refs[id]);
}
}
}
}
return ret;
},
processLoad: function(store, rightRecord, leftRecords, session) {
var ret = leftRecords;
if (session) {
// Allow infer here, we only get called when loading an associated store
ret = this.findRecords(session, rightRecord, leftRecords, true);
}
this.onLoadMany(rightRecord, ret, session);
return ret;
},
adoptAssociated: function(rightRecord, session) {
var store = this.getAssociatedItem(rightRecord),
leftRecords, i, len;
if (store) {
store.setSession(session);
leftRecords = store.getData().items;
for (i = 0, len = leftRecords.length; i < len; ++i) {
session.adopt(leftRecords[i]);
}
}
},
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, true);
};
},
createSetter: null, // no setter for an isMany side
onAddToMany: function(store, leftRecords) {
var rightRecord = store.getAssociatedEntity();
if (this.association.field) {
this.syncFK(leftRecords, rightRecord, false);
}
else {
this.setInstances(rightRecord, leftRecords);
}
},
onLoadMany: function(rightRecord, leftRecords, session) {
this.setInstances(rightRecord, leftRecords, session);
},
onRemoveFromMany: function(store, leftRecords) {
if (this.association.field) {
this.syncFK(leftRecords, store.getAssociatedEntity(), true);
}
else {
this.setInstances(null, leftRecords);
}
},
read: function(rightRecord, node, fromReader, readOptions) {
var me = this,
// We use the inverse role here since we're setting ourselves
// on the other record
instanceName = me.inverse.getInstanceName(),
leftRecords = me.callParent([rightRecord, node, fromReader, readOptions]),
store, len, i;
if (leftRecords) {
// Create the store and dump the data
store = rightRecord[me.getterName](null, null, leftRecords);
// Inline associations should *not* arrive on the "data" object:
delete rightRecord.data[me.role];
leftRecords = store.getData().items;
for (i = 0, len = leftRecords.length; i < len; ++i) {
leftRecords[i][instanceName] = rightRecord;
}
}
},
setInstances: function(rightRecord, leftRecords, session) {
var instanceName = this.inverse.getInstanceName(),
id = rightRecord ? rightRecord.getId() : null,
field = this.association.field,
len = leftRecords.length,
i, leftRecord, oldId, data, name;
for (i = 0; i < len; ++i) {
leftRecord = leftRecords[i];
leftRecord[instanceName] = rightRecord;
if (field) {
name = field.name;
data = leftRecord.data;
oldId = data[name];
if (oldId !== id) {
data[name] = id;
if (session) {
session.updateReference(leftRecord, field, id, oldId);
}
}
}
}
},
syncFK: function(leftRecords, rightRecord, clearing) {
// We are called to set things like the FK (ticketId) of an array of Comment
// entities. The best way to do that is call the setter on the Comment to set
// the Ticket. Since we are setting the Ticket, the name of that setter is on
// our inverse role.
var foreignKeyName = this.association.getFieldName(),
inverse = this.inverse,
setter = inverse.setterName, // setTicket
instanceName = inverse.getInstanceName(),
i = leftRecords.length,
id = rightRecord.getId(),
different, leftRecord, val;
while (i-- > 0) {
leftRecord = leftRecords[i];
different = !leftRecord.isEqual(id, leftRecord.get(foreignKeyName));
val = clearing ? null : rightRecord;
if (different !== clearing) {
// clearing === true
// different === true :: leave alone (not associated anymore)
// ** different === false :: null the value (no longer associated)
//
// clearing === false
// ** different === true :: set the value (now associated)
// different === false :: leave alone (already associated)
//
leftRecord.changingKey = true;
leftRecord[setter](val);
leftRecord.changingKey = false;
}
else {
// Ensure we set the instance, we may only have the key
leftRecord[instanceName] = val;
}
}
}
}),
Right: Ext.define(null, {
extend: 'Ext.data.schema.Role',
left: false,
side: 'right',
onDrop: function(leftRecord, session) {
// By virtue of being dropped, this record will be removed
// from any stores it belonged to. The only case we have
// to worry about is if we have a session but were not yet
// part of any stores, so we need to clear the foreign key.
var field = this.association.field;
if (field) {
leftRecord.set(field.name, null);
}
leftRecord[this.getInstanceName()] = null;
},
createGetter: function() {
// As the target of the FK (say "ticket" for the Comment entity) this
// getter is responsible for getting the entity referenced by the FK value.
var me = this;
return function(options, scope) {
// 'this' refers to the Comment instance inside this function
return me.doGetFK(this, options, scope);
};
},
createSetter: function() {
var me = this;
return function(rightRecord, options, scope) {
// 'this' refers to the Comment instance inside this function
return me.doSetFK(this, rightRecord, options, scope);
};
},
checkMembership: function(session, leftRecord) {
var field = this.association.field,
store;
if (field) {
store = this.getSessionStore(session, leftRecord.get(field.name));
// Check we're not in the middle of an add to the store.
if (store && !store.contains(leftRecord)) {
store.add(leftRecord);
}
}
},
onValueChange: function(leftRecord, session, newValue, oldValue) {
// If we have a session, we may be able to find the new store this belongs to
// If not, the best we can do is to remove the record from the associated store/s.
var me = this,
instanceName = me.getInstanceName(),
cls = me.cls,
hasNewValue, joined, store, i, associated, rightRecord;
if (!leftRecord.changingKey) {
hasNewValue = newValue || newValue === 0;
if (!hasNewValue) {
leftRecord[instanceName] = null;
}
if (session) {
// Find the store that holds this record and remove it if possible.
store = me.getSessionStore(session, oldValue);
if (store) {
store.remove(leftRecord);
}
// If we have a new value, try and find it and push it into the new store.
if (hasNewValue) {
store = me.getSessionStore(session, newValue);
if (store && !store.isLoading()) {
store.add(leftRecord);
}
if (cls) {
rightRecord = session.peekRecord(cls, newValue);
}
// Setting to undefined is important so that we can load the record later.
leftRecord[instanceName] = rightRecord || undefined;
}
}
else {
joined = leftRecord.joined;
if (joined) {
// Loop backwards because the store remove may cause unjoining, which means
// removal from the joined array.
for (i = joined.length - 1; i >= 0; i--) {
store = joined[i];
if (store.isStore) {
associated = store.getAssociatedEntity();
if (associated && associated.self === me.cls &&
associated.getId() === oldValue) {
store.remove(leftRecord);
}
}
}
}
}
}
if (me.owner && newValue === null) {
me.association.schema.queueKeyCheck(leftRecord, me);
}
},
checkKeyForDrop: function(leftRecord) {
var field = this.association.field;
if (leftRecord.get(field.name) === null) {
leftRecord.drop();
}
},
getSessionStore: function(session, value) {
// May not have the cls loaded yet
var cls = this.cls,
rec;
if (cls) {
rec = session.peekRecord(cls, value);
if (rec) {
return this.inverse.getAssociatedItem(rec);
}
}
},
read: function(leftRecord, node, fromReader, readOptions) {
var rightRecords = this.callParent([leftRecord, node, fromReader, readOptions]),
rightRecord;
if (rightRecords) {
rightRecord = rightRecords[0];
if (rightRecord) {
leftRecord[this.getInstanceName()] = rightRecord;
delete leftRecord.data[this.role];
}
}
}
})
});