// ========================================================================== // Project: SproutCore - JavaScript Application Framework // Copyright: ©2006-2011 Strobe Inc. and contributors. // Portions ©2008-2011 Apple Inc. All rights reserved. // License: Licensed under MIT license (see license.js) // ========================================================================== sc_require('models/record'); /** @class The Store is where you can find all of your dataHashes. Stores can be chained for editing purposes and committed back one chain level at a time all the way back to a persistent data source. Every application you create should generally have its own store objects. Once you create the store, you will rarely need to work with the store directly except to retrieve records and collections. Internally, the store will keep track of changes to your json data hashes and manage syncing those changes with your data source. A data source may be a server, local storage, or any other persistent code. @extends SC.Object @since SproutCore 1.0 */ SC.Store = SC.Object.extend( /** @scope SC.Store.prototype */ { /** An (optional) name of the store, which can be useful during debugging, especially if you have multiple nested stores. @type String */ name: null, /** An array of all the chained stores that current rely on the receiver store. @type Array */ nestedStores: null, /** The data source is the persistent storage that will provide data to the store and save changes. You normally will set your data source when you first create your store in your application. @type SC.DataSource */ dataSource: null, /** This type of store is not nested. @default NO @type Boolean */ isNested: NO, /** This type of store is not nested. @default NO @type Boolean */ commitRecordsAutomatically: NO, // .......................................................... // DATA SOURCE SUPPORT // /** Convenience method. Sets the current data source to the passed property. This will also set the store property on the dataSource to the receiver. If you are using this from the `core.js` method of your app, you may need to just pass a string naming your data source class. If this is the case, then your data source will be instantiated the first time it is requested. @param {SC.DataSource|String} dataSource the data source @returns {SC.Store} receiver */ from: function(dataSource) { this.set('dataSource', dataSource); return this ; }, // lazily convert data source to real object _getDataSource: function() { var ret = this.get('dataSource'); if (typeof ret === SC.T_STRING) { ret = SC.requiredObjectForPropertyPath(ret); if (ret.isClass) ret = ret.create(); this.set('dataSource', ret); } return ret; }, /** Convenience method. Creates a `CascadeDataSource` with the passed data source arguments and sets the `CascadeDataSource` as the data source for the receiver. @param {SC.DataSource...} dataSource one or more data source arguments @returns {SC.Store} receiver */ cascade: function(dataSource) { var dataSources; // Fast arguments access. // Accessing `arguments.length` is just a Number and doesn't materialize the `arguments` object, which is costly. dataSources = new Array(arguments.length); // SC.A(arguments) for (var i = 0, len = dataSources.length; i < len; i++) { dataSources[i] = arguments[i]; } dataSource = SC.CascadeDataSource.create({ dataSources: dataSources }); return this.from(dataSource); }, // .......................................................... // STORE CHAINING // /** Returns a new nested store instance that can be used to buffer changes until you are ready to commit them. When you are ready to commit your changes, call `commitChanges()` or `destroyChanges()` and then `destroy()` when you are finished with the chained store altogether. store = MyApp.store.chain(); .. edit edit edit store.commitChanges().destroy(); @param {Hash} attrs optional attributes to set on new store @param {Class} newStoreClass optional the class of the newly-created nested store (defaults to SC.NestedStore) @returns {SC.NestedStore} new nested store chained to receiver */ chain: function(attrs, newStoreClass) { if (!attrs) attrs = {}; attrs.parentStore = this; if (newStoreClass) { // Ensure the passed-in class is a type of nested store. if (SC.typeOf(newStoreClass) !== 'class') throw new Error("%@ is not a valid class".fmt(newStoreClass)); if (!SC.kindOf(newStoreClass, SC.NestedStore)) throw new Error("%@ is not a type of SC.NestedStore".fmt(newStoreClass)); } else { newStoreClass = SC.NestedStore; } // Replicate parent records references attrs.childRecords = this.childRecords ? SC.clone(this.childRecords) : {}; attrs.parentRecords = this.parentRecords ? SC.clone(this.parentRecords) : {}; var ret = newStoreClass.create(attrs), nested = this.nestedStores; if (!nested) nested = this.nestedStores = []; nested.push(ret); return ret ; }, /** Creates an autonomous nested store that is connected to the data source. Use this kind of nested store to ensure that all records that are committed into main store are first of all committed on the server. ns = store.chainAutonomousStore(); ... perform changes into the nested store ns.commitRecords( ...,callback ); or... newRecord.commitRecord( ..., callback ); into the callback method: A. If using a transaction model: "all or nothing" where all rows are handled into a single transaction if the transaction is successful, commit the successful changes into the main store and destroy: ns.commitSuccessfulChanges(); ns.destroy(); if not successful, handle the error, allow corrections if needed, etc. B. If using a transaction model: "independent rows" where the edited rows are independent and committed separately into the backend database When only some of them have been accepted by the server, the overall transaction is only partially successful therefore the nested store will contain rows with different statuses. In such case, into the callback method only the accepted rows should be pushed to the main store using commitSuccessfulChanges() ns.commitSuccessfulChanges(); ... if some of the rows were not accepted by the backend, allow corrections, attempt another commit, etc.: ns.commitRecords( ...,callback ); ... the callback will be invoked again Important note: In the case of such configuration, in order to preserve the consistency of the main store with the backend database, it is recommended to systematically call commitSuccessfulChanges() when receiving the answer from the server. @param {Hash} attrs optional attributes to set on new store @param {Class} newStoreClass optional the class of the newly-created nested store (defaults to SC.NestedStore) @returns {SC.NestedStore} new nested store chained to receiver */ chainAutonomousStore: function(attrs, newStoreClass) { var newAttrs = attrs ? SC.clone( attrs ) : {}; var source = this._getDataSource(); newAttrs.dataSource = source; return this.chain( newAttrs, newStoreClass ); }, /** @private Called by a nested store just before it is destroyed so that the parent can remove the store from its list of nested stores. @returns {SC.Store} receiver */ willDestroyNestedStore: function(nestedStore) { if (this.nestedStores) { this.nestedStores.removeObject(nestedStore); } return this ; }, /** Used to determine if a nested store belongs directly or indirectly to the receiver. @param {SC.Store} store store instance @returns {Boolean} YES if belongs */ hasNestedStore: function(store) { while(store && (store !== this)) store = store.get('parentStore'); return store === this ; }, // .......................................................... // SHARED DATA STRUCTURES // /** @private JSON data hashes indexed by store key. *IMPORTANT: Property is not observable* Shared by a store and its child stores until you make edits to it. @type Hash */ dataHashes: null, /** @private The current status of a data hash indexed by store key. *IMPORTANT: Property is not observable* Shared by a store and its child stores until you make edits to it. @type Hash */ statuses: null, /** @private This array contains the revisions for the attributes indexed by the storeKey. *IMPORTANT: Property is not observable* Revisions are used to keep track of when an attribute hash has been changed. A store shares the revisions data with its parent until it starts to make changes to it. @type Hash */ revisions: null, /** Array indicates whether a data hash is possibly in use by an external record for editing. If a data hash is editable then it may be modified at any time and therefore chained stores may need to clone the attributes before keeping a copy of them. Note that this is kept as an array because it will be stored as a dense array on some browsers, making it faster. @type Array */ editables: null, /** A set of storeKeys that need to be committed back to the data source. If you call `commitRecords()` without passing any other parameters, the keys in this set will be committed instead. @type SC.Set */ changelog: null, /** An array of `SC.Error` objects associated with individual records in the store (indexed by store keys). Errors passed form the data source in the call to dataSourceDidError() are stored here. @type Array */ recordErrors: null, /** A hash of `SC.Error` objects associated with queries (indexed by the GUID of the query). Errors passed from the data source in the call to `dataSourceDidErrorQuery()` are stored here. @type Hash */ queryErrors: null, /** A hash of child Records and there immediate parents */ childRecords: null, /** A hash of parent records with registered children */ parentRecords: null, // .......................................................... // CORE ATTRIBUTE API // // The methods in this layer work on data hashes in the store. They do not // perform any changes that can impact records. Usually you will not need // to use these methods. /** Returns the current edit status of a store key. May be one of `EDITABLE` or `LOCKED`. Used mostly for unit testing. @param {Number} storeKey the store key @returns {Number} edit status */ storeKeyEditState: function(storeKey) { var editables = this.editables; return (editables && editables[storeKey]) ? SC.Store.EDITABLE : SC.Store.LOCKED ; }, /** Returns the data hash for the given `storeKey`. This will also 'lock' the hash so that further edits to the parent store will no longer be reflected in this store until you reset. @param {Number} storeKey key to retrieve @returns {Hash} data hash or null */ readDataHash: function(storeKey) { return this.dataHashes[storeKey]; }, /** Returns the data hash for the `storeKey`, cloned so that you can edit the contents of the attributes if you like. This will do the extra work to make sure that you only clone the attributes one time. If you use this method to modify data hash, be sure to call `dataHashDidChange()` when you make edits to record the change. @param {Number} storeKey the store key to retrieve @returns {Hash} the attributes hash */ readEditableDataHash: function(storeKey) { // read the value - if there is no hash just return; nothing to do var ret = this.dataHashes[storeKey]; if (!ret) return ret ; // nothing to do. // clone data hash if not editable var editables = this.editables; if (!editables) editables = this.editables = []; if (!editables[storeKey]) { editables[storeKey] = 1 ; // use number to store as dense array ret = this.dataHashes[storeKey] = SC.clone(ret, YES); } return ret; }, /** Reads a property from the hash - cloning it if needed so you can modify it independently of any parent store. This method is really only well tested for use with toMany relationships. Although it is public you generally should not call it directly. @param {Number} storeKey storeKey of data hash @param {String} propertyName property to read @returns {Object} editable property value */ readEditableProperty: function(storeKey, propertyName) { var hash = this.readEditableDataHash(storeKey), editables = this.editables[storeKey], // get editable info... ret = hash[propertyName]; // editables must be made into a hash so that we can keep track of which // properties have already been made editable if (editables === 1) editables = this.editables[storeKey] = {}; // clone if needed if (!editables[propertyName]) { ret = hash[propertyName]; if (ret && ret.isCopyable) ret = hash[propertyName] = ret.copy(YES); editables[propertyName] = YES ; } return ret ; }, /** Replaces the data hash for the `storeKey`. This will lock the data hash and mark them as cloned. This will also call `dataHashDidChange()` for you. Note that the hash you set here must be a different object from the original data hash. Once you make a change here, you must also call `dataHashDidChange()` to register the changes. If the data hash does not yet exist in the store, this method will add it. Pass the optional status to edit the status as well. @param {Number} storeKey the store key to write @param {Hash} hash the new hash @param {String} status the new hash status @returns {SC.Store} receiver */ writeDataHash: function(storeKey, hash, status) { // update dataHashes and optionally status. if (hash) this.dataHashes[storeKey] = hash; if (status) this.statuses[storeKey] = status ; // also note that this hash is now editable var editables = this.editables; if (!editables) editables = this.editables = []; editables[storeKey] = 1 ; // use number for dense array support var processedPaths={}; // Update the child record hashes in place. if (!SC.none(this.parentRecords) ) { var children = this.parentRecords[storeKey] || {}, childHash; for (var key in children) { if (children.hasOwnProperty(key)) { if (hash) { var childPath = children[key]; childPath = childPath.split('.'); if (childPath.length > 1) { childHash = hash[childPath[0]][childPath[1]]; } else { childHash = hash[childPath[0]]; } if (!processedPaths[hash[childPath[0]]]){ // update data hash: required to push changes beyond the first nesting level this.writeDataHash(key, childHash, status); } if (childPath.length > 1 && ! processedPaths[hash[childPath[0]]]) { // save it so that we don't processed it over and over processedPaths[hash[childPath[0]]]=true; // force fetching of all children records by invoking the children_attribute wrapper code // and then interating the list in an empty loop // Ugly, but there's basically no other way to do it at the moment, other than // leaving this broken as it was before var that = this; this.invokeLast(function() { that.records[storeKey].get(childPath[0]).forEach(function (it) {}); }); } } else { this.writeDataHash(key, null, status); } } } } return this ; }, /** Removes the data hash from the store. This does not imply a deletion of the record. You could be simply unloading the record. Either way, removing the dataHash will be synced back to the parent store but not to the server. Note that you can optionally pass a new status to go along with this. If you do not pass a status, it will change the status to `SC.RECORD_EMPTY` (assuming you just unloaded the record). If you are deleting the record you may set it to `SC.Record.DESTROYED_CLEAN`. Be sure to also call `dataHashDidChange()` to register this change. @param {Number} storeKey @param {String} status optional new status @returns {SC.Store} receiver */ removeDataHash: function(storeKey, status) { // don't use delete -- that will allow parent dataHash to come through this.dataHashes[storeKey] = null; this.statuses[storeKey] = status || SC.Record.EMPTY; // hash is gone and therefore no longer editable var editables = this.editables; if (editables) editables[storeKey] = 0 ; return this ; }, /** Reads the current status for a storeKey. This will also lock the data hash. If no status is found, returns `SC.RECORD_EMPTY`. @param {Number} storeKey the store key @returns {Number} status */ readStatus: function(storeKey) { // use readDataHash to handle optimistic locking. this could be inlined // but for now this minimized copy-and-paste code. this.readDataHash(storeKey); return this.statuses[storeKey] || SC.Record.EMPTY; }, /** Reads the current status for the storeKey without actually locking the record. Usually you won't need to use this method. It is mostly used internally. @param {Number} storeKey the store key @returns {Number} status */ peekStatus: function(storeKey) { return this.statuses[storeKey] || SC.Record.EMPTY; }, /** Writes the current status for a storeKey. If the new status is `SC.Record.ERROR`, you may also pass an optional error object. Otherwise this param is ignored. @param {Number} storeKey the store key @param {String} newStatus the new status @param {SC.Error} error optional error object @returns {SC.Store} receiver */ writeStatus: function(storeKey, newStatus) { var that = this, ret; // use writeDataHash for now to handle optimistic lock. maximize code // reuse. ret = this.writeDataHash(storeKey, null, newStatus); this._propagateToChildren(storeKey, function(storeKey) { that.writeStatus(storeKey, newStatus); }); return ret; }, /** Call this method whenever you modify some editable data hash to register with the Store that the attribute values have actually changed. This will do the book-keeping necessary to track the change across stores including managing locks. @param {Number|Array} storeKeys one or more store keys that changed @param {Number} rev optional new revision number. normally leave null @param {Boolean} statusOnly (optional) YES if only status changed @param {String} key that changed (optional) @returns {SC.Store} receiver */ dataHashDidChange: function(storeKeys, rev, statusOnly, key) { // update the revision for storeKey. Use generateStoreKey() because that // guarantees a universally (to this store hierarchy anyway) unique // key value. if (!rev) rev = SC.Store.generateStoreKey(); var isArray, len, idx, storeKey; isArray = SC.typeOf(storeKeys) === SC.T_ARRAY; if (isArray) { len = storeKeys.length; } else { len = 1; storeKey = storeKeys; } var that = this; for(idx=0;idx 0) this._notifyRecordArrays(storeKeys, recordTypes); storeKeys.clear(); hasDataChanges.clear(); records.clear(); // Provide full reference to overwrite this.recordPropertyChanges.propertyForStoreKeys = {}; return this; }, /** Resets the store content. This will clear all internal data for all records, resetting them to an EMPTY state. You generally do not want to call this method yourself, though you may override it. @returns {SC.Store} receiver */ reset: function() { // create a new empty data store this.dataHashes = {} ; this.revisions = {} ; this.statuses = {} ; this.records = {}; this.childRecords = {}; this.parentRecords = {}; // also reset temporary objects and errors this.chainedChanges = this.locks = this.editables = null; this.changelog = null ; this.recordErrors = null; this.queryErrors = null; var dataSource = this.get('dataSource'); if (dataSource && dataSource.reset) { dataSource.reset(); } var records = this.records, storeKey; if (records) { for(storeKey in records) { if (!records.hasOwnProperty(storeKey)) continue ; this._notifyRecordPropertyChange(parseInt(storeKey, 10), NO); } } // Also reset all pre-created recordArrays. var ra, raList = this.get('recordArrays'); if (raList) { while ((ra = raList.pop())) { ra.destroy(); } raList.clear(); this.set('recordArrays', null); } this.set('hasChanges', NO); }, /** @private Called by a nested store on a parent store to commit any changes from the store. This will copy any changed dataHashes as well as any persistent change logs. If the parentStore detects a conflict with the optimistic locking, it will raise an exception before it makes any changes. If you pass the force flag then this detection phase will be skipped and the changes will be applied even if another resource has modified the store in the mean time. @param {SC.Store} nestedStore the child store @param {SC.Set} changes the set of changed store keys @param {Boolean} force @returns {SC.Store} receiver */ commitChangesFromNestedStore: function(nestedStore, changes, force) { // first, check for optimistic locking problems if (!force) this._verifyLockRevisions(changes, nestedStore.locks); // OK, no locking issues. So let's just copy them changes. // get local reference to values. var len = changes.length, i, storeKey, myDataHashes, myStatuses, myEditables, myRevisions, myParentRecords, myChildRecords, chDataHashes, chStatuses, chRevisions, chParentRecords, chChildRecords; myRevisions = this.revisions ; myDataHashes = this.dataHashes; myStatuses = this.statuses; myEditables = this.editables ; myParentRecords = this.parentRecords ? this.parentRecords : this.parentRecords ={} ; myChildRecords = this.childRecords ? this.childRecords : this.childRecords = {} ; // setup some arrays if needed if (!myEditables) myEditables = this.editables = [] ; chDataHashes = nestedStore.dataHashes; chRevisions = nestedStore.revisions ; chStatuses = nestedStore.statuses; chParentRecords = nestedStore.parentRecords || {}; chChildRecords = nestedStore.childRecords || {}; for(i=0;i0) { ret = SC.RecordArray.create({ store: this, storeKeys: storeKeys }); } else ret = storeKeys; // empty array return ret ; }, /** @private */ _CACHED_REC_ATTRS: {}, /** @private */ _CACHED_REC_INIT: function() {}, /** Given a `storeKey`, return a materialized record. You will not usually call this method yourself. Instead it will used by other methods when you find records by id or perform other searches. If a `recordType` has been mapped to the storeKey, then a record instance will be returned even if the data hash has not been requested yet. Each Store instance returns unique record instances for each storeKey. @param {Number} storeKey The storeKey for the dataHash. @returns {SC.Record} Returns a record instance. */ materializeRecord: function(storeKey) { var records = this.records, ret, recordType, attrs; // look up in cached records if (!records) records = this.records = {}; // load cached records ret = records[storeKey]; if (ret) return ret; // not found -- OK, create one then. recordType = SC.Store.recordTypeFor(storeKey); if (!recordType) return null; // not recordType registered, nothing to do // Populate the attributes. attrs = this._CACHED_REC_ATTRS ; attrs.storeKey = storeKey ; attrs.store = this ; // We do a little gymnastics here to prevent record initialization before we've // received and cached a copy of the object. This is because if initialization // triggers downstream effects which call materializeRecord for the same record, // we won't have a copy of it cached yet, causing another copy to be created // and resulting in a stack overflow at best and a really hard-to-diagnose bug // involving two instances of the same record floating around at worst. // Override _object_init to prevent premature initialization. var _object_init = recordType.prototype._object_init; recordType.prototype._object_init = this._CACHED_REC_INIT; // Create the record (but don't init it). ret = records[storeKey] = recordType.create(); // Repopulate the _object_init method and run initialization. recordType.prototype._object_init = ret._object_init = _object_init; ret._object_init([attrs]); return ret ; }, // .......................................................... // CORE RECORDS API // // The methods in this section can be used to manipulate records without // actually creating record instances. /** Creates a new record instance with the passed `recordType` and `dataHash`. You can also optionally specify an id or else it will be pulled from the data hash. Example: MyApp.Record = SC.Record.extend({ attrA: SC.Record.attr(String, { defaultValue: 'def' }), isAttrB: SC.Record.attr(Boolean, { key: 'attr_b' }), primaryKey: 'pKey' }); // If you don't provide a value and have designated a defaultValue, the // defaultValue will be used. MyApp.store.createRecord(MyApp.Record).get('attributes'); > { attrA: 'def' } // If you use a key on an attribute, you can specify the key name or the // attribute name when creating the record, but if you specify both, only // the key name will be used. MyApp.store.createRecord(MyApp.Record, { isAttrB: YES }).get('attributes'); > { attr_b: YES } MyApp.store.createRecord(MyApp.Record, { attr_b: YES }).get('attributes'); > { attr_b: YES } MyApp.store.createRecord(MyApp.Record, { isAttrB: NO, attr_b: YES }).get('attributes'); > { attr_b: YES } Note that the record will not yet be saved back to the server. To save a record to the server, call `commitChanges()` on the store. @param {SC.Record} recordType the record class to use on creation @param {Hash} dataHash the JSON attributes to assign to the hash. @param {String} id (optional) id to assign to record @returns {SC.Record} Returns the created record */ createRecord: function(recordType, dataHash, id) { var primaryKey, prototype, storeKey, status, K = SC.Record, changelog, defaultVal, ret; //initialize dataHash if necessary dataHash = (dataHash ? dataHash : {}); // First, try to get an id. If no id is passed, look it up in the // dataHash. if (!id && (primaryKey = recordType.prototype.primaryKey)) { id = dataHash[primaryKey]; // if still no id, check if there is a defaultValue function for // the primaryKey attribute and assign that defaultVal = recordType.prototype[primaryKey] ? recordType.prototype[primaryKey].defaultValue : null; if(!id && SC.typeOf(defaultVal)===SC.T_FUNCTION) { id = dataHash[primaryKey] = defaultVal(); } } // Next get the storeKey - base on id if available storeKey = id ? recordType.storeKeyFor(id) : SC.Store.generateStoreKey(); // now, check the state and do the right thing. status = this.readStatus(storeKey); // check state // any busy or ready state or destroyed dirty state is not allowed if ((status & K.BUSY) || (status & K.READY) || (status === K.DESTROYED_DIRTY)) { throw id ? K.RECORD_EXISTS_ERROR : K.BAD_STATE_ERROR; // allow error or destroyed state only with id } else if (!id && (status===SC.DESTROYED_CLEAN || status===SC.ERROR)) { throw K.BAD_STATE_ERROR; } // Store the dataHash and setup initial status. this.writeDataHash(storeKey, dataHash, K.READY_NEW); // Register the recordType with the store. SC.Store.replaceRecordTypeFor(storeKey, recordType); this.dataHashDidChange(storeKey); // If the attribute wasn't provided in the dataHash, attempt to insert a // default value. We have to do this after materializing the record, // because the defaultValue property may be a function that expects // the record as an argument. ret = this.materializeRecord(storeKey); prototype = recordType.prototype; for (var prop in prototype) { var propPrototype = prototype[ prop ]; if (propPrototype && propPrototype.isRecordAttribute) { // Use the record attribute key if it is defined. var attrKey = propPrototype.key || prop; if (!dataHash.hasOwnProperty(attrKey)) { if (dataHash.hasOwnProperty(prop)) { // If the attribute key doesn't exist but the name does, fix it up. // (i.e. the developer has a record attribute `endDate` with a key // `end_date` on a record and when they created the record they // provided `endDate` not `end_date`) dataHash[ attrKey ] = dataHash[ prop ]; delete dataHash[ prop ]; } else { // If the attribute doesn't exist in the hash at all, check for a // default value to use instead. defaultVal = propPrototype.defaultValue; if (defaultVal) { if (SC.typeOf(defaultVal)===SC.T_FUNCTION) dataHash[ attrKey ] = SC.copy(defaultVal(ret, attrKey), YES); else dataHash[ attrKey ] = SC.copy(defaultVal, YES); } } } else if (attrKey !== prop && dataHash.hasOwnProperty(prop)) { // If both attrKey and prop are provided, use attrKey only. delete dataHash[ prop ]; } } } // Record is now in a committable state -- add storeKey to changelog changelog = this.changelog; if (!changelog) changelog = SC.Set.create(); changelog.add(storeKey); this.changelog = changelog; // if commit records is enabled if(this.get('commitRecordsAutomatically')){ this.invokeLast(this.commitRecords); } // Propagate the status to any aggregate records before returning. if (ret) ret.propagateToAggregates(); return ret; }, /** Creates an array of new records. You must pass an array of `dataHash`es plus a `recordType` and, optionally, an array of ids. This will create an array of record instances with the same record type. If you need to instead create a bunch of records with different data types you can instead pass an array of `recordType`s, one for each data hash. @param {SC.Record|Array} recordTypes class or array of classes @param {Array} dataHashes array of data hashes @param {Array} ids (optional) ids to assign to records @returns {Array} array of materialized record instances. */ createRecords: function(recordTypes, dataHashes, ids) { var ret = [], recordType, id, isArray, len = dataHashes.length, idx ; isArray = SC.typeOf(recordTypes) === SC.T_ARRAY; if (!isArray) recordType = recordTypes; for(idx=0;idx0; }, /** Commits the passed store keys or ids. If no `storeKey`s are given, it will commit any records in the changelog. Based on the current state of the record, this will ask the data source to perform the appropriate actions on the store keys. @param {Array} recordTypes the expected record types (SC.Record) @param {Array} ids to commit @param {SC.Set} storeKeys to commit @param {Hash} params optional additional parameters to pass along to the data source @param {Function|Array} callback function or array of callbacks @returns {Boolean} if the action was succesful. */ commitRecords: function(recordTypes, ids, storeKeys, params, callbacks) { var source = this._getDataSource(), isArray = SC.typeOf(recordTypes) === SC.T_ARRAY, hasCallbackArray = SC.typeOf(callbacks) === SC.T_ARRAY, retCreate= [], retUpdate= [], retDestroy = [], rev = SC.Store.generateStoreKey(), K = SC.Record, recordType, idx, storeKey, status, ret, len, callback; // If no params are passed, look up storeKeys in the changelog property. // Remove any committed records from changelog property. if(!recordTypes && !ids && !storeKeys){ storeKeys = this.changelog; } len = storeKeys ? storeKeys.get('length') : (ids ? ids.get('length') : 0); for(idx=0;idx0 || params)) { ret = source.commitRecords.call(source, this, retCreate, retUpdate, retDestroy, params); } //remove all committed changes from changelog if (ret && !recordTypes && !ids) { if (storeKeys === this.changelog) { this.changelog = null; } else { this.changelog.removeEach(storeKeys); } } return ret ; }, /** Commits the passed store key or id. Based on the current state of the record, this will ask the data source to perform the appropriate action on the store key. You have to pass either the id or the storeKey otherwise it will return NO. @param {SC.Record} recordType the expected record type @param {String} id the id of the record to commit @param {Number} storeKey the storeKey of the record to commit @param {Hash} params optional additional params that will passed down to the data source @param {Function|Array} callback function or array of functions @returns {Boolean} if the action was successful. */ commitRecord: function(recordType, id, storeKey, params, callback) { var array = this._TMP_RETRIEVE_ARRAY, ret ; if (id === undefined && storeKey === undefined ) return NO; if (storeKey !== undefined) { array[0] = storeKey; storeKey = array; id = null ; } else { array[0] = id; id = array; } ret = this.commitRecords(recordType, id, storeKey, params, callback); array.length = 0 ; return ret; }, /** Cancels an inflight request for the passed records. Depending on the server implementation, this could cancel an entire request, causing other records to also transition their current state. @param {SC.Record|Array} recordTypes class or array of classes @param {Array} ids ids to destroy @param {Array} storeKeys (optional) store keys to destroy @returns {SC.Store} the store. */ cancelRecords: function(recordTypes, ids, storeKeys) { var source = this._getDataSource(), isArray = SC.typeOf(recordTypes) === SC.T_ARRAY, K = SC.Record, ret = [], status, len, idx, id, recordType, storeKey; len = (storeKeys === undefined) ? ids.length : storeKeys.length; for(idx=0;idx= 0) { nestedStores[loc]._scstore_dataSourceDidFetchQuery(query); } return this ; }, /** Called by your data source if it cancels fetching the results of a query. This will put any RecordArray's back into its original state (READY or EMPTY). @param {SC.Query} query the query you cancelled @returns {SC.Store} receiver */ dataSourceDidCancelQuery: function(query) { return this._scstore_dataSourceDidCancelQuery(query, YES); }, _scstore_dataSourceDidCancelQuery: function(query, createIfNeeded) { var recArray = this._findQuery(query, createIfNeeded, NO), nestedStores = this.get('nestedStores'), loc = nestedStores ? nestedStores.get('length') : 0; // fix query if needed if (recArray) recArray.storeDidCancelQuery(query); // notify nested stores while(--loc >= 0) { nestedStores[loc]._scstore_dataSourceDidCancelQuery(query, NO); } return this ; }, /** Called by your data source if it encountered an error loading the query. This will put the query into an error state until you try to refresh it again. @param {SC.Query} query the query with the error @param {SC.Error} error [optional] an SC.Error instance to associate with query @returns {SC.Store} receiver */ dataSourceDidErrorQuery: function(query, error) { var errors = this.queryErrors; // Add the error to the array of query errors (for lookup later on if necessary). if (error && error.isError) { if (!errors) errors = this.queryErrors = {}; errors[SC.guidFor(query)] = error; } return this._scstore_dataSourceDidErrorQuery(query, YES); }, _scstore_dataSourceDidErrorQuery: function(query, createIfNeeded) { var recArray = this._findQuery(query, createIfNeeded, NO), nestedStores = this.get('nestedStores'), loc = nestedStores ? nestedStores.get('length') : 0; // fix query if needed if (recArray) recArray.storeDidErrorQuery(query); // notify nested stores while(--loc >= 0) { nestedStores[loc]._scstore_dataSourceDidErrorQuery(query, NO); } return this ; }, // .......................................................... // INTERNAL SUPPORT // /** @private */ init: function() { sc_super(); this.reset(); }, toString: function() { // Include the name if the client has specified one. var name = this.get('name'); if (!name) { return sc_super(); } else { var ret = sc_super(); return "%@ (%@)".fmt(name, ret); } }, // .......................................................... // PRIMARY KEY CONVENIENCE METHODS // /** Given a `storeKey`, return the `primaryKey`. @param {Number} storeKey the store key @returns {String} primaryKey value */ idFor: function(storeKey) { return SC.Store.idFor(storeKey); }, /** Given a storeKey, return the recordType. @param {Number} storeKey the store key @returns {SC.Record} record instance */ recordTypeFor: function(storeKey) { return SC.Store.recordTypeFor(storeKey) ; }, /** Given a `recordType` and `primaryKey`, find the `storeKey`. If the `primaryKey` has not been assigned a `storeKey` yet, it will be added. @param {SC.Record} recordType the record type @param {String} primaryKey the primary key @returns {Number} storeKey */ storeKeyFor: function(recordType, primaryKey) { return recordType.storeKeyFor(primaryKey); }, /** Given a `primaryKey` value for the record, returns the associated `storeKey`. As opposed to `storeKeyFor()` however, this method will **NOT** generate a new `storeKey` but returned `undefined`. @param {SC.Record} recordType the record type @param {String} primaryKey the primary key @returns {Number} a storeKey. */ storeKeyExists: function(recordType, primaryKey) { return recordType.storeKeyExists(primaryKey); }, /** Finds all `storeKey`s of a certain record type in this store and returns an array. @param {SC.Record} recordType @returns {Array} set of storeKeys */ storeKeysFor: function(recordType) { var ret = [], isEnum = recordType && recordType.isEnumerable, recType, storeKey, isMatch ; if (!this.statuses) return ret; for(storeKey in SC.Store.recordTypesByStoreKey) { recType = SC.Store.recordTypesByStoreKey[storeKey]; // if same record type and this store has it if (isEnum) isMatch = recordType.contains(recType); else isMatch = recType === recordType; if(isMatch && this.statuses[storeKey]) ret.push(parseInt(storeKey, 10)); } return ret; }, /** Finds all `storeKey`s in this store and returns an array. @returns {Array} set of storeKeys */ storeKeys: function() { var ret = [], storeKey; if(!this.statuses) return ret; for(storeKey in this.statuses) { // if status is not empty if(this.statuses[storeKey] !== SC.Record.EMPTY) { ret.push(parseInt(storeKey, 10)); } } return ret; }, /** Returns string representation of a `storeKey`, with status. @param {Number} storeKey @returns {String} */ statusString: function(storeKey) { var rec = this.materializeRecord(storeKey); return rec.statusString(); } }) ; SC.Store.mixin(/** @scope SC.Store.prototype */{ /** Standard error raised if you try to commit changes from a nested store and there is a conflict. @type Error */ CHAIN_CONFLICT_ERROR: new Error("Nested Store Conflict"), /** Standard error if you try to perform an operation on a nested store without a parent. @type Error */ NO_PARENT_STORE_ERROR: new Error("Parent Store Required"), /** Standard error if you try to perform an operation on a nested store that is only supported in root stores. @type Error */ NESTED_STORE_UNSUPPORTED_ERROR: new Error("Unsupported In Nested Store"), /** Standard error if you try to retrieve a record in a nested store that is dirty. (This is allowed on the main store, but not in nested stores.) @type Error */ NESTED_STORE_RETRIEVE_DIRTY_ERROR: new Error("Cannot Retrieve Dirty Record in Nested Store"), /** Data hash state indicates the data hash is currently editable @type String */ EDITABLE: 'editable', /** Data hash state indicates the hash no longer tracks changes from a parent store, but it is not editable. @type String */ LOCKED: 'locked', /** Data hash state indicates the hash is tracking changes from the parent store and is not editable. @type String */ INHERITED: 'inherited', /** @private This array maps all storeKeys to primary keys. You will not normally access this method directly. Instead use the `idFor()` and `storeKeyFor()` methods on `SC.Record`. */ idsByStoreKey: [], /** @private Maps all `storeKey`s to a `recordType`. Once a `storeKey` is associated with a `primaryKey` and `recordType` that remains constant throughout the lifetime of the application. */ recordTypesByStoreKey: {}, /** @private Maps some `storeKeys` to query instance. Once a `storeKey` is associated with a query instance, that remains constant through the lifetime of the application. If a `Query` is destroyed, it will remove itself from this list. Don't access this directly. Use queryFor(). */ queriesByStoreKey: [], /** @private The next store key to allocate. A storeKey must always be greater than 0 */ nextStoreKey: 1, /** Generates a new store key for use. @type Number */ generateStoreKey: function() { return this.nextStoreKey++; }, /** Given a `storeKey` returns the `primaryKey` associated with the key. If no `primaryKey` is associated with the `storeKey`, returns `null`. @param {Number} storeKey the store key @returns {String} the primary key or null */ idFor: function(storeKey) { return this.idsByStoreKey[storeKey] ; }, /** Given a `storeKey`, returns the query object associated with the key. If no query is associated with the `storeKey`, returns `null`. @param {Number} storeKey the store key @returns {SC.Query} query query object */ queryFor: function(storeKey) { return this.queriesByStoreKey[storeKey]; }, /** Given a `storeKey` returns the `SC.Record` class associated with the key. If no record type is associated with the store key, returns `null`. The SC.Record class will only be found if you have already called storeKeyFor() on the record. @param {Number} storeKey the store key @returns {SC.Record} the record type */ recordTypeFor: function(storeKey) { return this.recordTypesByStoreKey[storeKey]; }, /** Swaps the `primaryKey` mapped to the given storeKey with the new `primaryKey`. If the `storeKey` is not currently associated with a record this will raise an exception. @param {Number} storeKey the existing store key @param {String} newPrimaryKey the new primary key @returns {SC.Store} receiver */ replaceIdFor: function(storeKey, newId) { var oldId = this.idsByStoreKey[storeKey], recordType, storeKeys; if (oldId !== newId) { // skip if id isn't changing recordType = this.recordTypeFor(storeKey); if (!recordType) { throw new Error("replaceIdFor: storeKey %@ does not exist".fmt(storeKey)); } // map one direction... this.idsByStoreKey[storeKey] = newId; // then the other... storeKeys = recordType.storeKeysById() ; delete storeKeys[oldId]; storeKeys[newId] = storeKey; } return this ; }, /** Swaps the `recordType` recorded for a given `storeKey`. Normally you should not call this method directly as it can damage the store behavior. This method is used by other store methods to set the `recordType` for a `storeKey`. @param {Integer} storeKey the store key @param {SC.Record} recordType a record class @returns {SC.Store} receiver */ replaceRecordTypeFor: function(storeKey, recordType) { this.recordTypesByStoreKey[storeKey] = recordType; return this ; } });