// ========================================================================== // Project: SproutCore - JavaScript Application Framework // Copyright: ©2006-2009 Sprout Systems, Inc. and contributors. // Portions ©2008-2009 Apple, Inc. All rights reserved. // License: Licened 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 array of all the chained stores that current rely on the receiver store. @property {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. */ dataSource: null, /** This type of store is not nested. */ isNested: 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. @returns {SC.Store} receiver */ from: function(dataSource) { this.set('dataSource', dataSource); return this ; }, /** 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} reciever */ cascade: function(dataSource) { var dataSources = SC.A(arguments) ; 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(); }}} @returns {SC.NestedStore} new nested store chained to receiver */ chain: function() { var ret = SC.NestedStore.create({ parentStore: this }) ; var nested = this.nestedStores; if (!nested) nested =this.nestedStores = []; nested.push(ret); return ret ; }, /** @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 ; }, // .......................................................... // 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. @property {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. @property {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. @property {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. @property {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. @property {Hash} */ changelog: 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 storekey. 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, locks = this.locks; 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); } 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 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. Eitherway, 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} reciever */ removeDataHash: function(storeKey, status) { var rev ; // don't use delete -- that will allow parent dataHash to come through this.dataHashes[storeKey] = null; this.statuses[storeKey] = status || SC.Record.EMPTY; rev = this.revisions[storeKey] = this.revisions[storeKey]; // copy ref // 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 {String} 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; }, /** Writes the current status for a storeKey. */ writeStatus: function(storeKey, newStatus) { // use writeDataHash for now to handle optimistic lock. maximize code // reuse. return this.writeDataHash(storeKey, null, newStatus); }, /** 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 @returns {SC.Store} receiver */ dataHashDidChange: function(storeKeys, rev, statusOnly) { // update the revision for storeKey. Use generateStoreKey() because that // gaurantees 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; } for(idx=0;idx0) { ret = SC.RecordArray.create({ store: this, storeKeys: storeKeys }); } else ret = storeKeys; // empty array return ret ; }, _TMP_REC_ATTRS: {}, /** 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 {Integer} 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 attrs = this._TMP_REC_ATTRS ; attrs.storeKey = storeKey ; attrs.store = this ; ret = records[storeKey] = recordType.create(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. 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, storeKey, status, K = SC.Record, changelog; // 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]; } // 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; } // add dataHash and setup initial status -- also save recordType this.writeDataHash(storeKey, dataHash, K.READY_NEW); SC.Store.replaceRecordTypeFor(storeKey, recordType); this.dataHashDidChange(storeKey); // 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; // finally return materialized record return this.materializeRecord(storeKey) ; }, /** Creates an array of new records. You must pass an array of dataHashes 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 recordTypes, 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;idx