/**
* @private
*/
Ext.define('Ext.data.schema.Role', {
/**
* @property {Ext.data.schema.Association} association
* @readonly
*/
isRole: true,
/**
* @property {Boolean} left
* @readonly
*/
left: true,
/**
* @property {Boolean} owner
* @readonly
*/
owner: false,
/**
* @property {String} side
* @readonly
*/
side: 'left',
/**
* @property {Boolean} isMany
* @readonly
*/
isMany: false,
/**
* @property {Ext.Class} cls
* The `Ext.data.Model` derived class.
* @readonly
*/
/**
* @property {Ext.data.schema.Role} inverse
* @readonly
*/
/**
* @property {String} type
* The `{@link Ext.data.Model#entityName}` derived class.
* @readonly
*/
/**
* @property {String} role
* @readonly
*/
defaultReaderType: 'json',
_internalReadOptions: {
recordsOnly: true,
asRoot: true
},
constructor: function(association, config) {
var me = this,
extra = config.extra;
Ext.apply(me, config);
if (extra) {
extra = Ext.apply({}, extra);
delete extra.type;
Ext.apply(me, extra);
delete me.extra;
}
me.association = association;
// The Association's owner property starts as either "left" or "right" (a string)
// and we promote it to a reference to the appropriate Role instance here.
if (association.owner === me.side) {
association.owner = me;
me.owner = true;
}
},
processUpdate: function() {
Ext.raise('Only the "many" for an association may be processed. "' + this.role +
'" is not valid.');
},
/**
* Exclude any locally modified records that don't belong in the store. Include locally
* modified records that should be in the store. Also correct any foreign keys that
* need to be updated.
*
* @param {Ext.data.Store} store The store.
* @param {Ext.data.Model} associatedEntity The entity that owns the records.
* @param {Ext.data.Model[]} records The records to check.
* @param {Ext.data.Session} session The session holding the records
* @return {Ext.data.Model[]} The corrected set of records.
*
* @private
*/
processLoad: function(store, associatedEntity, records, session) {
return records;
},
/**
* @method
*
* Check whether a record belongs to any stores when it is added to the session.
*
* @param {Ext.data.Session} session The session
* @param {Ext.data.Model} record The model being added to the session
* @private
*/
checkMembership: Ext.emptyFn,
/**
* Adopt the associated items when a record is adopted.
* @param {Ext.data.Model} record The record being adopted.
* @param {Ext.data.Session} session The session being adopted into
*
* @private
*/
adoptAssociated: function(record, session) {
var other = this.getAssociatedItem(record);
if (other) {
session.adopt(other);
}
},
$roleFilterId: '$associationRoleFilter',
createAssociationStore: function(session, from, records, isComplete) {
var me = this,
association = me.association,
foreignKeyName = association.getFieldName(),
isMany = association.isManyToMany,
storeConfig = me.storeConfig,
id = from.getId(),
config = {
// Always want immediate load
asynchronousLoad: false,
model: me.cls,
role: me,
session: session,
associatedEntity: from,
disableMetaChangeEvent: true,
pageSize: null,
remoteFilter: true,
trackRemoved: !session
},
store;
if (isMany) {
// For many-to-many associations each role has a field
config.filters = [{
id: me.$roleFilterId,
property: me.inverse.field, // @TODO filterProperty
value: id,
exactMatch: true
}];
}
else if (foreignKeyName) {
config.filters = [{
id: me.$roleFilterId,
property: foreignKeyName, // @TODO filterProperty
value: id,
exactMatch: true
}];
config.foreignKeyName = foreignKeyName;
}
if (storeConfig) {
Ext.apply(config, storeConfig);
}
store = Ext.Factory.store(config);
me.onStoreCreate(store, session, id);
// Want to run these in all cases for M-1, only with a session M-M
if (!isMany || session) {
store.on({
scope: me,
add: 'onAddToMany',
remove: 'onRemoveFromMany',
clear: 'onRemoveFromMany'
});
}
if (records) {
store.loadData(records);
}
store.complete = !!isComplete;
return store;
},
onStoreCreate: Ext.emptyFn,
getAssociatedStore: function(inverseRecord, options, scope, records, allowInfer) {
// Consider the Comment entity with a ticketId to a Ticket entity. The Comment
// is on the left (the FK holder's side) so we are implementing the guts of
// the comments() method to load the Store of Comment entities. This trek
// begins from a Ticket (inverseRecord).
var me = this,
storeName = me.getStoreName(),
store = inverseRecord[storeName],
hadStore = store,
session = inverseRecord.session,
load = options && options.reload,
source = inverseRecord.$source,
isComplete = false,
phantom = false,
hadSourceStore, args, i, len, raw,
rec, sourceStore, hadRecords, isLoading;
if (!store) {
if (session) {
// We want to check whether we can automatically get the store contents from the
// parent session. For this to occur, we need to have a parent in the session,
// and the store needs to be created and loaded with the initial dataset.
if (source) {
phantom = source.phantom;
}
if (!records && source) {
sourceStore = source[storeName];
if (sourceStore && !sourceStore.isLoading()) {
records = [];
raw = sourceStore.getData().items;
for (i = 0, len = raw.length; i < len; ++i) {
rec = raw[i];
records.push(session.getRecord(rec.self, rec.id));
}
isComplete = !!sourceStore.complete;
hadSourceStore = true;
}
}
if (!hadSourceStore) {
// We'll only hit here if we didn't have a usable source
hadRecords = !!records;
records = me.findRecords(session, inverseRecord, records, allowInfer);
if (!hadRecords && (!records || !records.length)) {
records = null;
}
isComplete = phantom || hadRecords;
}
}
else {
// As long as we had the collection exist, we're complete, even if it's empty.
isComplete = !!records;
}
// If the inverse is a phantom, we can't be loading any data so we're complete
store = me.createAssociationStore(session, inverseRecord, records,
isComplete || inverseRecord.phantom);
store.$source = sourceStore;
if (!records && (me.autoLoad || options)) {
load = true;
}
inverseRecord[storeName] = store;
}
if (options) {
// We need to trigger a load or the store is already loading. Defer
// callbacks until that happens
if (load || store.isLoading()) {
store.on('load', function(store, records, success, operation) {
args = [store, operation];
scope = scope || options.scope || inverseRecord;
if (success) {
Ext.callback(options.success, scope, args);
}
else {
Ext.callback(options.failure, scope, args);
}
args.push(success);
Ext.callback(options, scope, args);
Ext.callback(options.callback, scope, args);
}, null, { single: true });
}
else {
// Trigger straight away
args = [store, null];
scope = scope || options.scope || inverseRecord;
Ext.callback(options.success, scope, args);
args.push(true);
Ext.callback(options, scope, args);
Ext.callback(options.callback, scope, args);
}
}
isLoading = store.isLoading();
if (load) {
if (!isLoading) {
store.load();
}
}
else if (hadStore && records && !isLoading) {
store.loadData(records);
}
return store;
},
/**
* Gets the store/record associated with this role from an existing record.
* Will only return if the value is loaded.
*
* @param {Ext.data.Model} rec The record
*
* @return {Ext.data.Model/Ext.data.Store} The associated item. `null` if not loaded.
* @private
*/
getAssociatedItem: function(rec) {
var key = this.isMany ? this.getStoreName() : this.getInstanceName();
return rec[key] || null;
},
onDrop: Ext.emptyFn,
onIdChanged: Ext.emptyFn,
getReaderRoot: function() {
var me = this;
return me.associationKey ||
(me.associationKey = me.association.schema.getNamer().readerRoot(me.role));
},
getReader: function() {
var me = this,
reader = me.reader,
Model = me.cls,
useSimpleAccessors = !me.associationKey,
root = this.getReaderRoot();
if (reader && !reader.isReader) {
if (Ext.isString(reader)) {
reader = {
type: reader
};
}
Ext.applyIf(reader, {
model: Model,
rootProperty: root,
useSimpleAccessors: useSimpleAccessors,
type: me.defaultReaderType
});
reader = me.reader = Ext.createByAlias('reader.' + reader.type, reader);
}
return reader;
},
getInstanceName: function() {
var me = this;
return me.instanceName ||
(me.instanceName = me.association.schema.getNamer().instanceName(me.role));
},
getOldInstanceName: function() {
return this.oldInstanceName ||
(this.oldInstanceName = '$old' + this.getInstanceName());
},
getStoreName: function() {
var me = this;
return me.storeName ||
(me.storeName = me.association.schema.getNamer().storeName(me.role));
},
constructReader: function(fromReader) {
var me = this,
reader = me.getReader(),
Model = me.cls,
useSimpleAccessors = !me.associationKey,
root = me.getReaderRoot(),
proxyReader, proxy;
// No reader supplied
if (!reader) {
proxy = Model.getProxy();
// if the associated model has a Reader already, use that, otherwise attempt to
// create a sensible one
if (proxy) {
proxyReader = proxy.getReader();
reader = new proxyReader.self();
reader.copyFrom(proxyReader);
reader.setRootProperty(root);
}
else {
reader = new fromReader.self({
model: Model,
useSimpleAccessors: useSimpleAccessors,
rootProperty: root
});
}
me.reader = reader;
}
return reader;
},
read: function(record, data, fromReader, readOptions) {
var reader = this.constructReader(fromReader),
root = reader.getRoot(data),
inverse = this.inverse,
inverseName = inverse && !inverse.isMany && inverse.getInstanceName(),
recordCreator =
(readOptions && readOptions.recordCreator) || reader.defaultRecordCreator;
// If we have an inverseName for a non isMany association, we define a custom recordCreator.
// This custom recordCreator will capture a reference to the parent record that will be
// available in the Ext.data.Model#constructor method so that it can add the parent record
// to the model instance. This then makes the parent record available in a field's convert
// method during a Model.loadData() or Model.load() call.
if (inverseName) {
readOptions = Ext.applyIf({
recordCreator: function(data, Model) {
if (!data.$parentRecordRef) {
data.$parentRecordRef = [inverseName, record];
}
return recordCreator(data, Model);
}
}, readOptions);
}
if (root) {
return reader.readRecords(root, readOptions, this._internalReadOptions);
}
},
getCallbackOptions: function(options, scope, defaultScope) {
if (typeof options === 'function') {
options = {
callback: options,
scope: scope || defaultScope
};
}
else if (options) {
options = Ext.apply({}, options);
options.scope = scope || options.scope || defaultScope;
}
return options;
},
doGetFK: function(leftRecord, options, scope) {
// Consider the Department entity with a managerId to a User entity. This method
// is the guts of the getManager method that we place on the Department entity to
// acquire a User entity. We are the "manager" role and that role describes a
// User. This method is called, however, given a Department (leftRecord) as the
// start of this trek.
var me = this, // the "manager" role
cls = me.cls, // User
foreignKey = me.association.getFieldName(), // "managerId"
instanceName = me.getInstanceName(), // "manager"
rightRecord = leftRecord[instanceName], // = department.manager
reload = options && options.reload,
done = rightRecord !== undefined && !reload,
session = leftRecord.session,
foreignKeyId, args;
if (!done) {
// We don't have the User record yet, so try to get it.
if (session) {
foreignKeyId = leftRecord.get(foreignKey);
if (foreignKeyId || foreignKeyId === 0) {
done = session.peekRecord(cls, foreignKeyId, true) && !reload;
rightRecord = session.getRecord(cls, foreignKeyId, false);
}
else {
done = true;
leftRecord[instanceName] = rightRecord = null;
}
}
else if (foreignKey) {
// The good news is that we do indeed have a FK so we can do a load using
// the value of the FK.
foreignKeyId = leftRecord.get(foreignKey);
if (!foreignKeyId && foreignKeyId !== 0) {
// A value of null ends that hope though... but we still need to do
// some callbacks perhaps.
done = true;
leftRecord[instanceName] = rightRecord = null;
}
else {
// foreignKeyId is the managerId from the Department (record), so
// make a new User, set its idProperty and load the real record via
// User.load method.
if (!rightRecord) {
// We may be reloading, let's check if we have one.
rightRecord = cls.createWithId(foreignKeyId);
}
// we are not done in this case, so don't set "done"
}
}
else {
// Without a FK value by which to request the User record, we cannot do
// anything. Declare victory and get out.
done = true;
rightRecord = null;
}
}
else if (rightRecord) {
// If we're still loading, call load again which will handle the extra callbacks.
done = !rightRecord.isLoading();
}
if (done) {
if (options) {
args = [rightRecord, null];
scope = scope || options.scope || leftRecord;
Ext.callback(options.success, scope, args);
args.push(true);
Ext.callback(options, scope, args);
Ext.callback(options.callback, scope, args);
}
}
else {
leftRecord[instanceName] = rightRecord;
options = me.getCallbackOptions(options, scope, leftRecord);
rightRecord.load(options);
}
return rightRecord;
},
doSetFK: function(leftRecord, rightRecord, options, scope) {
// Consider the Department entity with a managerId to a User entity. This method
// is the guts of the setManager method that we place on the Department entity to
// store the User entity. We are the "manager" role and that role describes a
// User. This method is called, however, given a Department (record) and the User
// (value).
var me = this,
foreignKey = me.association.getFieldName(), // "managerId"
instanceName = me.getInstanceName(), // "manager"
current = leftRecord[instanceName],
inverse = me.inverse,
inverseSetter = inverse.setterName, // setManagerDepartment for User
modified, oldInstanceName;
if (rightRecord && rightRecord.isEntity) {
if (current !== rightRecord) {
oldInstanceName = me.getOldInstanceName();
leftRecord[oldInstanceName] = current;
leftRecord[instanceName] = rightRecord;
if (current && current.isEntity) {
current[inverse.getInstanceName()] = undefined;
}
if (foreignKey) {
leftRecord.set(foreignKey, rightRecord.getId());
}
delete leftRecord[oldInstanceName];
leftRecord.onAssociatedRecordSet(rightRecord, me);
if (inverseSetter) {
// Because the rightRecord has a reference back to the leftRecord
// we pass on to its setter (if there is one). We've already set
// the value on this side so we won't recurse back-and-forth.
rightRecord[inverseSetter](leftRecord);
}
}
}
else {
// The value we received could just be the id of the rightRecord so we just
// need to set the FK accordingly and cleanup any cached references.
//<debug>
if (!foreignKey) {
Ext.raise('No foreignKey specified for "' + me.association.left.role +
'" by ' + leftRecord.$className);
}
//</debug>
modified = (leftRecord.changingKey && !inverse.isMany) ||
leftRecord.set(foreignKey, rightRecord);
// set returns the modifiedFieldNames[] or null if nothing was change
if (modified && current && current.isEntity &&
!current.isEqual(current.getId(), rightRecord)) {
// If we just modified the FK value and it no longer matches the id of the
// record we had cached (ret), remove references from *both* sides:
leftRecord[instanceName] = undefined;
if (!inverse.isMany) {
current[inverse.getInstanceName()] = undefined;
}
}
}
if (options) {
if (Ext.isFunction(options)) {
options = {
callback: options,
scope: scope || leftRecord
};
}
return leftRecord.save(options);
}
}
});