(function(exports) { // ========================================================================== // Project: SproutCore IndexSet // Copyright: ©2011 Strobe Inc. and contributors. // License: Licensed under MIT license (see license.js) // ========================================================================== /*globals sc_assert */ var get = Ember.get, set = Ember.set, abs = Math.abs; function isIndexSet(obj) { return obj instanceof Ember.IndexSet; } /** @private iterates through a named range, setting hints every HINT_SIZE indexes pointing to the nearest range start. The passed range must start on a range boundary. It can end anywhere. */ function _hint(indexSet, start, length, content) { if (content === undefined) content = indexSet._content; var skip = Ember.IndexSet.HINT_SIZE, next = abs(content[start]), // start of next range loc = start - (start % skip) + skip, // next hint loc lim = start + length ; // stop while (loc < lim) { // make sure we are in current rnage while ((next !== 0) && (next <= loc)) { start = next ; next = abs(content[start]) ; } // past end if (next === 0) { delete content[loc]; // do not change if on actual boundary } else if (loc !== start) { content[loc] = start ; // set hint } loc += skip; } } /** @private Walks a content array and copies its contents to a new array. For large content arrays this is faster than using slice() */ function _sliceContent(c) { if (c.length < 1000) return c.slice(); // use native when faster var cur = 0, ret = [], next = c[0]; while(next !== 0) { ret[cur] = next ; cur = (next<0) ? (0-next) : next ; next = c[cur]; } ret[cur] = 0; _hint(this, 0, cur, ret); // hints are not copied manually - add them return ret ; } /** @class A collection of ranges. You can use an IndexSet to keep track of non- continuous ranges of items in a parent array. IndexSet's are used for selection, for managing invalidation ranges and other data-propogation. Examples --- var set = Ember.IndexSet.create(ranges) ; set.contains(index); set.add(index, length); set.remove(index, length); // uses a backing Ember.Array object to return each index set.forEach(function(object) { .. }) // returns the index set.forEachIndex(function(index) { ... }); // returns ranges set.forEachRange(function(start, length) { .. }); Implementation Notes --- An IndexSet stores indices on the object. A positive value great than the index tells you the end of an occupied range. A negative values tells you the end of an empty range. A value less than the index is a search accelerator. It tells you the start of the nearest range. @extends Ember.Enumerable @extends Ember.MutableEnumerable @extends Ember.Copyable @extends Ember.Freezable @since SproutCore 1.0 */ Ember.IndexSet = Ember.Object.extend(Ember.Enumerable, Ember.MutableEnumerable, Ember.Freezable, Ember.Copyable, /** @scope Ember.IndexSet.prototype */ { /** Walk like a duck. You should use instanceof instead. @deprecated @type Boolean @default YES */ isIndexSet: YES, /** Total number of indexes contained in the set @type Number */ length: 0, /** One greater than the largest index currently stored in the set. This is sometimes useful when determining the total range of items covering the index set. @type Number */ max: 0, /** The first index included in the set or -1. @type Number */ min: function() { var content = this._content, cur = content[0]; return (cur === 0) ? -1 : (cur>0) ? 0 : abs(cur); }.property('[]').cacheable(), /** When you create a new index set you can optional pass another index set or a starting range to be added to the set. */ init: function(start, length) { this._super(); // optimized method to clone an index set. if (start && isIndexSet(start)) { this._content = _sliceContent(start._content); set(this, 'max', get(start, 'max')); set(this, 'length', get(start, 'length')); set(this, 'source', get(start, 'source')); // otherwise just do a regular add } else { this._content = [0]; if (start !== undefined) this.add(start, length); } }, /** Returns the first index in the set . @type Number */ firstObject: function() { return get(this, 'length')>0 ? get(this, 'min') : undefined; }.property(), /** Returns the starting index of the nearest range for the specified index. @param {Number} index @returns {Number} starting index */ rangeStartForIndex: function(index) { var content = this._content, max = get(this, 'max'), ret, next, accel; // fast cases if (index >= max) return max ; if (abs(content[index]) > index) return index ; // we hit a border // use accelerator to find nearest content range accel = index - (index % Ember.IndexSet.HINT_SIZE); ret = content[accel]; if (ret<0 || ret>index) ret = accel; next = abs(content[ret]); // now step forward through ranges until we find one that includes the // index. while (next < index) { ret = next ; next = abs(content[ret]); } return ret ; }, /** Returns YES if the passed index set contains the exact same indexes as the receiver. If you pass any object other than an index set, returns NO. @param {Object} obj another object. @returns {Boolean} */ isEqual: function(obj) { // optimize for some special cases if (obj === this) return YES ; if (!obj || !isIndexSet(obj) || (get(obj, 'max') !== get(this, 'max')) || (get(obj, 'length') !== get(this, 'length'))) return NO; // ok, now we need to actually compare the ranges of the two. var lcontent = this._content, rcontent = obj._content, cur = 0, next = lcontent[cur]; do { if (rcontent[cur] !== next) return NO ; cur = abs(next) ; next = lcontent[cur]; } while (cur !== 0); return YES ; }, /** Returns the first index in the set before the passed index or null if there are no previous indexes in the set. @param {Number} index index to check @returns {Number} index or -1 */ indexBefore: function(index) { if (index===0) return -1; // fast path index--; // start with previous index var content = this._content, max = get(this, 'max'), start = this.rangeStartForIndex(index); if (!content) return null; // loop backwards until we find a range that is in the set. while((start===max) || (content[start]<0)) { if (start === 0) return -1 ; // nothing before; just quit index = start -1 ; start = this.rangeStartForIndex(index); } return index; }, /** Returns the first index in the set after the passed index or null if there are no additional indexes in the set. @param {Number} index index to check @returns {Number} index or -1 */ indexAfter: function(index) { var content = this._content, max = get(this, 'max'), start, next ; if (!content || (index>=max)) return -1; // fast path index++; // start with next index // loop forwards until we find a range that is in the set. start = this.rangeStartForIndex(index); next = content[start]; while(next<0) { if (next === 0) return -1 ; //nothing after; just quit index = start = abs(next); next = content[start]; } return index; }, /** Returns YES if the index set contains the named index @param {Number} start index or range @param {Number} length optional range length @returns {Boolean} */ contains: function(start, length) { var content, cur, next, rstart, rnext; // normalize input if (length === undefined) { if (start === null || start === undefined) return NO ; if ('number' === typeof start) { length = 1 ; // if passed an index set, check each receiver range } else if (start && isIndexSet(start)) { if (start === this) return YES ; // optimization content = start._content ; cur = 0 ; next = content[cur]; while (next !== 0) { if ((next>0) && !this.contains(cur, next-cur)) return NO ; cur = abs(next); next = content[cur]; } return YES ; // passed just a hash range } else { length = start.length; start = start.start; } } rstart = this.rangeStartForIndex(start); rnext = this._content[rstart]; return (rnext>0) && (rstart <= start) && (rnext >= (start+length)); }, /** Returns YES if the index set contains any of the passed indexes. You can pass a single index, a range or an index set. @param {Number} start index, range, or IndexSet @param {Number} length optional range length @returns {Boolean} */ intersects: function(start, length) { var content, cur, next, lim; // normalize input if (length === undefined) { if ('number' === typeof start) { length = 1 ; // if passed an index set, check each receiver range } else if (start && isIndexSet(start)) { if (start === this) return YES ; // optimization content = start._content ; cur = 0 ; next = content[cur]; while (next !== 0) { if ((next>0) && this.intersects(cur, next-cur)) return YES ; cur = abs(next); next = content[cur]; } return NO ; } else { length = start.length; start = start.start; } } cur = this.rangeStartForIndex(start); content = this._content; next = content[cur]; lim = start + length; while (cur < lim) { if (next === 0) return NO; // no match and at end! if ((next > 0) && (next > start)) return YES ; // found a match cur = abs(next); next = content[cur]; } return NO ; // no match }, /** Returns a new IndexSet without the passed range or indexes. This is a convenience over simply cloning and removing. Does some optimizations. @param {Number} start index, range, or IndexSet @param {Number} length optional range length @returns {Ember.IndexSet} new index set */ without: function(start, length) { if (start === this) return new Ember.IndexSet(); // just need empty set return this.copy().remove(start, length); }, /** Replace the index set's current content with the passed index set. This is faster than clearing the index set adding the values again. It is useful for when you want to reuse an existing index set. @param {Number} start index, Range, or another IndexSet @param {Number} length optional length of range. @returns {Ember.IndexSet} receiver */ replace: function(start, length) { if (length === undefined) { if ('number' === typeof start) { length = 1 ; } else if (start && isIndexSet(start)) { var oldLen = get(this, 'length'), newLen = get(start, 'length'); this.enumerableContentWillChange(oldLen, newLen); Ember.beginPropertyChanges(this); this._content = _sliceContent(start._content); set(this, 'max', get(start, 'max')); set(this, 'length', newLen); set(this, 'source', get(start, 'source')); Ember.endPropertyChanges(this); this.enumerableContentDidChange(oldLen, newLen); return this ; } else { length = start.length; start = start.start; } } var oldlen = this.length; this._content.length=1; this._content[0] = 0; this.length = this.max = 0 ; // reset without notifying since add() return this.add(start, length); }, /** Adds the specified range of indexes to the set. You can also pass another IndexSet to union the contents of the index set with the receiver. @param {Number} start index, Range, or another IndexSet @param {Number} length optional length of range. @returns {Ember.IndexSet} receiver */ add: function(start, length) { if (get(this, 'isFrozen')) throw new Error(Ember.FROZEN_ERROR); var content, cur, next, notified; // normalize IndexSet input if (start && isIndexSet(start)) { start.forEachRange(this.add, this); return this ; } else if (length === undefined) { if (start === null || start === undefined) { return this; // nothing to do } else if ('number' === typeof start) { length = 1 ; } else { length = start.length; start = start.start; } } else if (length === null) length = 1 ; // if no length - do nothing. - note captures when length != number if (!(length > 0)) return this; // special case - appending to end of set var max = get(this, 'max'), oldmax = max, delta, value ; content = this._content ; if (start === max) { this.enumerableContentWillChange(); notified = true; // if adding to the end and the end is in set, merge. if (start > 0) { cur = this.rangeStartForIndex(start-1); next = content[cur]; // just extend range at end if (next > 0) { delete content[max]; // no 0 content[cur] = max = start + length ; start = cur ; // previous range was not in set, just tack onto the end } else { content[max] = max = start + length; } } else { content[start] = max = length; } content[max] = 0 ; set(this, 'max', max); set(this, 'length', get(this, 'length') + length) ; length = max - start ; // past end of last range, just add as a new range. } else if (start > max) { this.enumerableContentWillChange(); notified = true; content[max] = 0-start; // empty! content[start] = start+length ; content[start+length] = 0; // set end set(this, 'max', start + length) ; set(this, 'length', this.length + length) ; // affected range goes from starting range to end of content. length = start + length - max ; start = max ; // otherwise, merge into existing range } else { // find nearest starting range. split or join that range cur = this.rangeStartForIndex(start); next = content[cur]; max = start + length ; delta = 0 ; // we are right on a boundary and we had a range or were the end, then // go back one more. if ((start>0) && (cur === start) && (next <= 0)) { cur = this.rangeStartForIndex(start-1); next = content[cur] ; } // previous range is not in set. splice it here if (next < 0) { content[cur] = 0-start ; // if previous range extends beyond this range, splice afterwards also if (abs(next) > max) { content[start] = 0-max; content[max] = next ; } else content[start] = next; // previous range is in set. merge the ranges } else { start = cur ; if (next > max) { // delta -= next - max ; max = next ; } } // at this point there should be clean starting point for the range. // just walk the ranges, adding up the length delta and then removing // the range until we find a range that passes last cur = start; while (cur < max) { // get next boundary. splice if needed - if value is 0, we are at end // just skip to last value = content[cur]; if (value === 0) { content[max] = 0; next = max ; delta += max - cur ; if (!notified && delta>0) { this.enumerableContentWillChange(); notified = true; } } else { next = abs(value); if (next > max) { content[max] = value ; next = max ; } // ok, cur range is entirely inside top range. // add to delta if needed if (value < 0) { delta += next - cur ; if (!notified && delta>0) { this.enumerableContentWillChange(); notified = true; } } } delete content[cur] ; // and remove range cur = next; } // cur should always === last now. if the following range is in set, // merge in also - don't adjust delta because these aren't new indexes if ((cur = content[max]) > 0) { delete content[max]; max = cur ; } // finally set my own range. content[start] = max ; if (max > oldmax) set(this, 'max', max) ; // adjust length set(this, 'length', get(this, 'length') + delta); // compute hint range length = max - start ; } _hint(this, start, length); if (notified) this.enumerableContentDidChange(); return this; }, /** Removes the specified range of indexes from the set @param {Number} start index, Range, or IndexSet @param {Number} length optional length of range. @returns {Ember.IndexSet} receiver */ remove: function(start, length) { if (get(this, 'isFrozen')) throw new Error(Ember.FROZEN_ERROR); // normalize input if (length === undefined) { if (start === null || start === undefined) { return this; // nothing to do } else if ('number' === typeof start) { length = 1 ; // if passed an index set, just add each range in the index set. } else if (isIndexSet(start)) { start.forEachRange(this.remove, this); return this; } else { length = start.length; start = start.start; } } if (!(length > 0)) return this; // handles when length != number this.enumerableContentWillChange(); // special case - appending to end of set var max = get(this, 'max'), oldmax = max, content = this._content, cur, next, delta, value, last ; // if we're past the end, do nothing. if (start >= max) return this; // find nearest starting range. split or join that range cur = this.rangeStartForIndex(start); next = content[cur]; last = start + length ; delta = 0 ; // we are right on a boundary and we had a range or were the end, then // go back one more. if ((start>0) && (cur === start) && (next > 0)) { cur = this.rangeStartForIndex(start-1); next = content[cur] ; } // previous range is in set. splice it here if (next > 0) { content[cur] = start ; // if previous range extends beyond this range, splice afterwards also if (next > last) { content[start] = last; content[last] = next ; } else content[start] = next; // previous range is not in set. merge the ranges } else { start = cur ; next = abs(next); if (next > last) { last = next ; } } // at this point there should be clean starting point for the range. // just walk the ranges, adding up the length delta and then removing // the range until we find a range that passes last cur = start; while (cur < last) { // get next boundary. splice if needed - if value is 0, we are at end // just skip to last value = content[cur]; if (value === 0) { content[last] = 0; next = last ; } else { next = abs(value); if (next > last) { content[last] = value ; next = last ; } // ok, cur range is entirely inside top range. // add to delta if needed if (value > 0) delta += next - cur ; } delete content[cur] ; // and remove range cur = next; } // cur should always === last now. if the following range is not in set, // merge in also - don't adjust delta because these aren't new indexes if ((cur = content[last]) < 0) { delete content[last]; last = abs(cur) ; } // set my own range - if the next item is 0, then clear it. if (content[last] === 0) { delete content[last]; content[start] = 0 ; set(this, 'max', start); //max has changed } else { content[start] = 0-last ; } // adjust length set(this, 'length', get(this, 'length') - delta); // compute hint range length = last - start ; _hint(this, start, length); this.enumerableContentDidChange(); return this; }, /** Clears the set */ clear: function() { if (get(this, 'isFrozen')) throw new Error(Ember.FROZEN_ERROR); var oldLen = get(this, 'length'); if (oldLen>0) this.enumerableContentWillChange(); Ember.beginPropertyChanges(this); this._content.length=1; this._content[0] = 0; set(this, 'length', 0); set(this, 'max', 0); Ember.endPropertyChanges(this); if (oldLen > 0) this.enumerableContentDidChange(); }, /** Add all the ranges in the passed array. @param {Enumerable} objects The list of ranges you want to add */ addEach: function(objects) { if (get(this, 'isFrozen')) throw new Error(Ember.FROZEN_ERROR); Ember.beginPropertyChanges(this); objects.forEach(function(idx) { this.add(idx); }, this); Ember.endPropertyChanges(this); return this ; }, /** Removes all the ranges in the passed array. @param {Object...} objects The list of objects you want to remove */ removeEach: function(objects) { if (get(this, 'isFrozen')) throw new Error(Ember.FROZEN_ERROR); Ember.beginPropertyChanges(this); objects.forEach(function(idx) { this.remove(idx); }, this); Ember.endPropertyChanges(this); return this ; }, /** Clones the set into a new set. */ copy: function() { return new Ember.IndexSet(this); }, /** @private (nodoc) */ clone: Ember.alias('copy'), /** @private (nodoc) */ slice: Ember.alias('copy'), /** Returns a string describing the internal range structure. Useful for debugging. @returns {String} */ inspect: function() { var content = this._content, len = content.length, idx = 0, ret = [], item; for(idx=0;idx".fmt(ret.join(' , ')); }, /** Invoke the callback, passing each occuppied range instead of each index. This can be a more efficient way to iterate in some cases. The callback should have the signature: callback(start, length, indexSet, source) { ... } If you pass a target as a second option, the callback will be called in the target context. @param {Function} callback The method to run on each iteration @param {Object} target the object to call the callback on @returns {Ember.IndexSet} receiver */ forEachRange: function(callback, target) { var content = this._content, cur = 0, next = content[cur], source = this.source; if (target === undefined) target = null ; while (next !== 0) { if (next > 0) callback.call(target, cur, next - cur, this, source); cur = abs(next); next = content[cur]; } return this ; }, /** Invokes the callback for each index within the passed start/length range. Otherwise works just like regular forEach(). @param {Number} start starting index @param {Number} length length of range @param {Function} callback @param {Object} target @returns {Ember.IndexSet} receiver */ forEachIn: function(start, length, callback, target) { var content = this._content, cur = 0, idx = 0, lim = start + length, source = this.source, next = content[cur]; if (target === undefined) target = null ; while (next !== 0) { if (cur < start) cur = start ; // skip forward while((cur < next) && (cur < lim)) { callback.call(target, cur++, idx++, this, source); } if (cur >= lim) { cur = next = 0 ; } else { cur = abs(next); next = content[cur]; } } return this ; }, /** Total number of indexes within the specified range. @param {Number|Ember.IndexSet} start index, range object or IndexSet @param {Number} length optional range length @returns {Number} count of indexes */ lengthIn: function(start, length) { var ret = 0 ; // normalize input if (length === undefined) { if (start === null || start === undefined) { return 0; // nothing to do } else if ('number' === typeof start) { length = 1 ; // if passed an index set, just add each range in the index set. } else if (isIndexSet(start)) { start.forEachRange(function(start, length) { ret += this.lengthIn(start, length); }, this); return ret; } else { length = start.length; start = start.start; } } // fast path if (get(this, 'length') === 0) return 0; var content = this._content, cur = 0, next = content[cur], lim = start + length ; while (cur0) { ret += (next>lim) ? lim-cur : next-cur; } cur = abs(next); next = content[cur]; } return ret ; }, // .......................................................... // OBJECT API // /** Optionally set the source property on an index set and then you can iterate over the actual object values referenced by the index set. See indexOf(), lastIndexOf(), forEachObject(), addObject() and removeObject(). */ source: null, /** Returns the first index in the set that matches the passed object. You must have a source property on the set for this to work. @param {Object} object the object to check @param {Number} startAt optional starting point @returns {Number} found index or -1 if not in set */ indexOf: function(object, startAt) { var source = get(this, 'source'); if (!source) throw "%@.indexOf() requires source".fmt(this); var len = get(source, 'length'), // start with the first index in the set content = this._content, cur = content[0]<0 ? abs(content[0]) : 0, idx ; while(cur>=0 && cur= len) cur = len-1; while (cur>=0) { idx = source.lastIndexOf(object, cur); if (idx<0) return -1 ; // not found in source if (this.contains(idx)) return idx; // found in source and in set. cur = idx+1; } return -1; // not found }, /** Iterates through the objects at each index location in the set. You must have a source property on the set for this to work. The callback you pass will be invoked for each object in the set with the following signature: function callback(object, index, source, indexSet) { ... } If you pass a target, it will be used when the callback is called. @param {Function} callback function to invoke. @param {Object} target optional content. otherwise uses window @returns {Ember.IndexSet} receiver */ forEachObject: function(callback, target) { var source = get(this, 'source'); if (!source) throw "%@.forEachObject() requires source".fmt(this); var content = this._content, cur = 0, idx = 0, next = content[cur]; if (target === undefined) target = null ; while (next !== 0) { while(cur < next) { callback.call(target, source.objectAt(cur), cur, source, this); cur++; } cur = abs(next); next = content[cur]; } return this ; }, /** Adds all indexes where the object appears to the set. If firstOnly is passed, then it will find only the first index and add it. If you know the object only appears in the source array one time, firstOnly may make this method faster. Requires source to work. @param {Object} object the object to add @param {Boolean} firstOnly Set to true if you can assume that the first match is the only one @returns {Ember.IndexSet} receiver */ addObject: function(object, firstOnly) { var source = get(this, 'source'); sc_assert("%@.addObject() requires source".fmt(this), !!source); var len = get(source, 'length'), cur = 0, idx; while(cur>=0 && cur= 0) { this.add(idx); if (firstOnly) return this ; cur = idx++; } else return this ; } return this ; }, /** Adds any indexes matching the passed objects. If firstOnly is passed, then only finds the first index for each object. @param {Ember.Enumerable} objects the objects to add @param {Boolean} firstOnly Set to true if you can assume that the first match is the only one @returns {Ember.IndexSet} receiver */ addObjects: function(objects, firstOnly) { objects.forEach(function(object) { this.addObject(object, firstOnly); }, this); return this; }, /** Removes all indexes where the object appears to the set. If firstOnly is passed, then it will find only the first index and add it. If you know the object only appears in the source array one time, firstOnly may make this method faster. Requires source to work. @param {Object} object the object to add @param {Boolean} firstOnly Set to true if you can assume that the first match is the only one @returns {Ember.IndexSet} receiver */ removeObject: function(object, firstOnly) { var source = get(this, 'source'); sc_assert("%@.removeObject() requires source".fmt(this), !!source); var len = source.get('length'), cur = 0, idx; while(cur>=0 && cur= 0) { this.remove(idx); if (firstOnly) return this ; cur = idx+1; } else return this ; } return this ; }, /** Removes any indexes matching the passed objects. If firstOnly is passed, then only finds the first index for each object. @param {Ember.Enumerable} objects the objects to add @param {Boolean} firstOnly Set to true if you can assume that the first match is the only one @returns {Ember.IndexSet} receiver */ removeObjects: function(objects, firstOnly) { objects.forEach(function(object) { this.removeObject(object, firstOnly); }, this); return this; }, // ....................................... // PRIVATE // /** Usually observing notifications from IndexSet are not useful, so supress them by default. @type Boolean @default NO */ LOG_OBSERVING: NO, /** @private - optimized call to forEach() */ forEach: function(callback, target) { var content = this._content, cur = 0, idx = 0, source = get(this, 'source'), next = content[cur]; if (target === undefined) target = null ; while (next !== 0) { while(cur < next) { callback.call(target, cur++, idx++, this, source); } cur = abs(next); next = content[cur]; } return this ; }, /** @private - support iterators */ nextObject: function(ignore, idx, context) { var content = this._content, next = context.next, max = get(this, 'max'); // next boundary // seed. if (idx === null) { idx = next = 0 ; } else if (idx >= max) { delete context.next; // cleanup context return null ; // nothing left to do } else idx++; // look on next index // look for next non-empty range if needed. if (idx === next) { do { idx = abs(next); next = content[idx]; } while(next < 0); context.next = next; } return idx; }, toString: function() { var str = []; this.forEachRange(function(start, length) { str.push(length === 1 ? start : "%@..%@".fmt(start, start + length - 1)); }, this); return "Ember.IndexSet<%@>".fmt(str.join(',')) ; } }) ; Ember.IndexSet.reopenClass({ /** Create can take a simple range as well.. */ create: function(start, length) { if ('number' === typeof start || isIndexSet(start)) { var C = this; return new C(start, length); } else { return this._super.apply(this, arguments); } }, /** @private Internal setting determines the preferred skip size for hinting sets. @type Number */ HINT_SIZE: 256, /** A empty index set. Useful for common comparisons. @type Ember.IndexSet */ EMPTY: new Ember.IndexSet().freeze() }); })({}); (function(exports) { })({}); (function(exports) { // ========================================================================== // 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) // ========================================================================== var get = Ember.get, set = Ember.set, getPath = Ember.getPath; /** @class This permits you to perform queries on your data store, written in a SQL-like language. Here is a simple example: q = Ember.Query.create({ conditions: "firstName = 'Jonny' AND lastName = 'Cash'" }) You can check if a certain record matches the query by calling q.contains(record) To find all records of your store, that match query q, use findAll with query q as argument: r = MyApp.store.findAll(q) `r` will be a record array containing all matching records. To limit the query to a record type of `MyApp.MyModel`, you can specify the type as a property of the query like this: q = Ember.Query.create({ conditions: "firstName = 'Jonny' AND lastName = 'Cash'", recordType: MyApp.MyModel }) Calling `find()` like above will now return only records of type t. It is recommended to limit your query to a record type, since the query will have to look for matching records in the whole store, if no record type is given. You can give an order, which the resulting records should follow, like this: q = Ember.Query.create({ conditions: "firstName = 'Jonny' AND lastName = 'Cash'", recordType: MyApp.MyModel, orderBy: "lastName, year DEEmber" }); The default order direction is ascending. You can change it to descending by writing `'DEEmber'` behind the property name like in the example above. If no order is given, or records are equal in respect to a given order, records will be ordered by guid. SproutCore Query Language ===== Features of the query language: Primitives: - record properties - `null`, `undefined` - `true`, `false` - numbers (integers and floats) - strings (double or single quoted) Parameters: - `%@` (wild card) - `{parameterName}` (named parameter) Wild cards are used to identify parameters by the order in which they appear in the query string. Named parameters can be used when tracking the order becomes difficult. Both types of parameters can be used by giving the parameters as a property to your query object: yourQuery.parameters = yourParameters where yourParameters should have one of the following formats: * for wild cards: `[firstParam, secondParam, thirdParam]` * for named params: `{name1: param1, mane2: parma2}` You cannot use both types of parameters in a single query! Operators: - `=` - `!=` - `<` - `<=` - `>` - `>=` - `BEGINS_WITH` -- (checks if a string starts with another one) - `ENDS_WITH` -- (checks if a string ends with another one) - `CONTAINS` -- (checks if a string contains another one, or if an object is in an array) - `MATCHES` -- (checks if a string is matched by a regexp, you will have to use a parameter to insert the regexp) - `ANY` -- (checks if the thing on its left is contained in the array on its right, you will have to use a parameter to insert the array) - `TYPE_IS` -- (unary operator expecting a string containing the name of a Model class on its right side, only records of this type will match) Boolean Operators: - `AND` - `OR` - `NOT` Parenthesis for grouping: - `(` and `)` Adding Your Own Query Handlers --- You can extend the query language with your own operators by calling: Ember.Query.registerQueryExtension('your_operator', your_operator_definition); See details below. As well you can provide your own comparison functions to control ordering of specific record properties like this: Ember.Query.registerComparison(property_name, comparison_for_this_property); Examples Some example queries: TODO add examples @extends Ember.Object @extends Ember.Copyable @extends Ember.Freezable @since SproutCore 1.0 */ Ember.Query = Ember.Object.extend(Ember.Copyable, Ember.Freezable, /** @scope Ember.Query.prototype */ { // .......................................................... // PROPERTIES // /** Walk like a duck. @type Boolean */ isQuery: YES, /** Unparsed query conditions. If you are handling a query yourself, then you will find the base query string here. @type String */ conditions: null, /** Optional orderBy parameters. This can be a string of keys, optionally beginning with the strings `"DEEmber "` or `"AEmber "` to select descending or ascending order. Alternatively, you can specify a comparison function, in which case the two records will be sent to it. Your comparison function, as with any other, is expected to return -1, 0, or 1. @type String | Function */ orderBy: null, /** The base record type or types for the query. This must be specified to filter the kinds of records this query will work on. You may either set this to a single record type or to an array or set of record types. @type Ember.Record */ recordType: null, /** Optional array of multiple record types. If the query accepts multiple record types, this is how you can check for it. @type Ember.Enumerable */ recordTypes: null, /** Returns the complete set of `recordType`s matched by this query. Includes any named `recordType`s plus their subclasses. @property @type Ember.Enumerable */ expandedRecordTypes: function() { var ret = Ember.Set.create(), rt, q ; if (rt = get(this, 'recordType')) this._scq_expandRecordType(rt, ret); else if (rt = get(this, 'recordTypes')) { rt.forEach(function(t) { this._scq_expandRecordType(t, ret); }, this); } else this._scq_expandRecordType(Ember.Record, ret); // save in queue. if a new recordtype is defined, we will be notified. q = Ember.Query._scq_queriesWithExpandedRecordTypes; if (!q) { q = Ember.Query._scq_queriesWithExpandedRecordTypes = Ember.Set.create(); } q.add(this); return ret.freeze() ; }.property('recordType', 'recordTypes').cacheable(), /** @private expands a single record type into the set. called recursively */ _scq_expandRecordType: function(recordType, set) { if (set.contains(recordType)) return; // nothing to do set.add(recordType); if (Ember.typeOf(recordType)==='string') { recordType = getPath( recordType); } recordType.subclasses.forEach(function(t) { this._scq_expandRecordType(t, set); }, this); }, /** Optional hash of parameters. These parameters may be interpolated into the query conditions. If you are handling the query manually, these parameters will not be used. @type Hash */ parameters: null, /** Indicates the location where the result set for this query is stored. Currently the available options are: - `Ember.Query.LOCAL` -- indicates that the query results will be automatically computed from the in-memory store. - `Ember.Query.REMOTE` -- indicates that the query results are kept on a remote server and hence must be loaded from the `DataSource`. The default setting for this property is `Ember.Query.LOCAL`. Note that even if a query location is `LOCAL`, your `DataSource` will still have its `fetch()` method called for the query. For `LOCAL` queries, you won't need to explicitly provide the query result set; you can just load records into the in-memory store as needed and let the query recompute automatically. If your query location is `REMOTE`, then your `DataSource` will need to provide the actual set of query results manually. Usually you will only need to use a `REMOTE` query if you are retrieving a large data set and you don't want to pay the cost of computing the result set client side. @type String */ location: 'local', // Ember.Query.LOCAL /** Another query that will optionally limit the search of records. This is usually configured for you when you do `find()` from another record array. @type Ember.Query */ scope: null, /** Returns `YES` if query location is Remote. This is sometimes more convenient than checking the location. @property @type Boolean */ isRemote: function() { return get(this, 'location') === Ember.Query.REMOTE; }.property('location').cacheable(), /** Returns `YES` if query location is Local. This is sometimes more convenient than checking the location. @property @type Boolean */ isLocal: function() { return get(this, 'location') === Ember.Query.LOCAL; }.property('location').cacheable(), /** Indicates whether a record is editable or not. Defaults to `NO`. Local queries should never be made editable. Remote queries may be editable or not depending on the data source. */ isEditable: NO, // .......................................................... // PRIMITIVE METHODS // /** Returns `YES` if record is matched by the query, `NO` otherwise. This is used when computing a query locally. @param {Ember.Record} record the record to check @param {Hash} parameters optional override parameters @returns {Boolean} YES if record belongs, NO otherwise */ contains: function(record, parameters) { // check the recordType if specified var rtype, ret = YES ; if (rtype = get(this, 'recordTypes')) { // plural form ret = rtype.find(function(t) { return (record instanceof t); }); } else if (rtype = get(this, 'recordType')) { // singular ret = (record instanceof rtype); } if (!ret) return NO ; // if either did not pass, does not contain // if we have a scope - check for that as well var scope = get(this, 'scope'); if (scope && !scope.contains(record)) return NO ; // now try parsing if (!this._isReady) this.parse(); // prepare the query if needed if (!this._isReady) return NO ; if (parameters === undefined) parameters = this.parameters || this; // if parsing worked we check if record is contained // if parsing failed no record will be contained return this._tokenTree.evaluate(record, parameters); }, /** Returns `YES` if the query matches one or more of the record types in the passed set. @param {Ember.Set} types set of record types @returns {Boolean} YES if record types match */ containsRecordTypes: function(types) { var rtype = get(this, 'recordType'); if (rtype) { return !!types.find(function(t) { return rtype.detect(t); }); } else if (rtype = get(this, 'recordTypes')) { return !!rtype.find(function(t) { return !!types.find(function(t2) { return t.detect(t2); }); }); } else return YES; // allow anything through }, /** Returns the sort order of the two passed records, taking into account the orderBy property set on this query. This method does not verify that the two records actually belong in the query set or not; this is checked using `contains()`. @param {Ember.Record} record1 the first record @param {Ember.Record} record2 the second record @returns {Number} -1 if record1 < record2, +1 if record1 > record2, 0 if equal */ compare: function(record1, record2) { // IMPORTANT: THIS CODE IS ALSO INLINED INSIDE OF THE 'compareStoreKeys' // CLASS METHOD. IF YOU CHANGE THIS IMPLEMENTATION, BE SURE // TO UPDATE IT THERE, TOO. // // (Any clients overriding this method will have their version called, // however. That's why we'll keep this here; clients might want to // override it and call this._super()). var result = 0, propertyName, order, len, i; // fast cases go here if (record1 === record2) return 0; // if called for the first time we have to build the order array if (!this._isReady) this.parse(); if (!this._isReady) { // can't parse. guid is wrong but consistent return Ember.compare(get(record1, 'id'),get(record2, 'id')); } // For every property specified in orderBy until non-eql result is found. // Or, if orderBy is a comparison function, simply invoke it with the // records. order = this._order; if (Ember.typeOf(order) === 'function') { result = order.call(null, record1, record2); } else { len = order ? order.length : 0; for (i=0; result===0 && (i < len); i++) { propertyName = order[i].propertyName; // if this property has a registered comparison use that if (Ember.Query.comparisons[propertyName]) { result = Ember.Query.comparisons[propertyName]( get(record1, propertyName),get(record2, propertyName)); // if not use default Ember.compare() } else { result = Ember.compare( get(record1, propertyName), get(record2, propertyName) ); } if ((result!==0) && order[i].descending) result = (-1) * result; } } // return result or compare by guid if (result !== 0) return result ; else return Ember.compare(get(record1, 'id'),get(record2, 'id')); }, /** @private Becomes YES once the query has been successfully parsed */ _isReady: NO, /** This method has to be called before the query object can be used. You will normaly not have to do this; it will be called automatically if you try to evaluate a query. You can, however, use this function for testing your queries. @returns {Boolean} true if parsing succeeded, false otherwise */ parse: function() { var conditions = get(this, 'conditions'), lang = get(this, 'queryLanguage'), tokens, tree; tokens = this._tokenList = this.tokenizeString(conditions, lang); tree = this._tokenTree = this.buildTokenTree(tokens, lang); this._order = this.buildOrder(get(this, 'orderBy')); this._isReady = !!tree && !tree.error; if (tree && tree.error) throw tree.error; return this._isReady; }, /** Returns the same query but with the scope set to the passed record array. This will copy the receiver. It also stores these queries in a cache to reuse them if possible. @param {Ember.RecordArray} recordArray the scope @returns {Ember.Query} new query */ queryWithScope: function(recordArray) { // look for a cached query on record array. var key = '__query__'+Ember.guidFor(this), ret = recordArray[key]; if (!ret) { recordArray[key] = ret = this.copy(); set(ret, 'scope', recordArray); ret.freeze(); } return ret ; }, // .......................................................... // PRIVATE SUPPORT // /** @private Properties that need to be copied when cloning the query. */ copyKeys: ['conditions', 'orderBy', 'recordType', 'recordTypes', 'parameters', 'location', 'scope'], /** @private */ concatenatedProperties: ['copyKeys'], /** @private Implement the Copyable API to clone a query object once it has been created. */ copy: function() { var opts = {}, keys = get(this, 'copyKeys'), loc = keys ? keys.length : 0, key, value, ret; while(--loc >= 0) { key = keys[loc]; value = get(this, key); if (value !== undefined) opts[key] = value ; } ret = this.constructor.create(opts); opts = null; return ret ; }, // .......................................................... // QUERY LANGUAGE DEFINITION // /** This is the definition of the query language. You can extend it by using `Ember.Query.registerQueryExtension()`. */ queryLanguage: { 'UNKNOWN': { firstCharacter: /[^\s'"\w\d\(\)\{\}]/, notAllowed: /[\-\s'"\w\d\(\)\{\}]/ }, 'PROPERTY': { firstCharacter: /[a-zA-Z_]/, notAllowed: /[^a-zA-Z_0-9\.]/, evalType: 'PRIMITIVE', /** @ignore */ evaluate: function (r,w) { return Ember.getPath(r, this.tokenValue); } }, 'NUMBER': { firstCharacter: /[\d\-]/, notAllowed: /[^\d\-\.]/, format: /^-?\d+$|^-?\d+\.\d+$/, evalType: 'PRIMITIVE', /** @ignore */ evaluate: function (r,w) { return parseFloat(this.tokenValue); } }, 'STRING': { firstCharacter: /['"]/, delimeted: true, evalType: 'PRIMITIVE', /** @ignore */ evaluate: function (r,w) { return this.tokenValue; } }, 'PARAMETER': { firstCharacter: /\{/, lastCharacter: '}', delimeted: true, evalType: 'PRIMITIVE', /** @ignore */ evaluate: function (r,w) { return w[this.tokenValue]; } }, '%@': { rememberCount: true, reservedWord: true, evalType: 'PRIMITIVE', /** @ignore */ evaluate: function (r,w) { return w[this.tokenValue]; } }, 'OPEN_PAREN': { firstCharacter: /\(/, singleCharacter: true }, 'CLOSE_PAREN': { firstCharacter: /\)/, singleCharacter: true }, 'AND': { reservedWord: true, leftType: 'BOOLEAN', rightType: 'BOOLEAN', evalType: 'BOOLEAN', /** @ignore */ evaluate: function (r,w) { var left = this.leftSide.evaluate(r,w); var right = this.rightSide.evaluate(r,w); return left && right; } }, 'OR': { reservedWord: true, leftType: 'BOOLEAN', rightType: 'BOOLEAN', evalType: 'BOOLEAN', /** @ignore */ evaluate: function (r,w) { var left = this.leftSide.evaluate(r,w); var right = this.rightSide.evaluate(r,w); return left || right; } }, 'NOT': { reservedWord: true, rightType: 'BOOLEAN', evalType: 'BOOLEAN', /** @ignore */ evaluate: function (r,w) { var right = this.rightSide.evaluate(r,w); return !right; } }, '=': { reservedWord: true, leftType: 'PRIMITIVE', rightType: 'PRIMITIVE', evalType: 'BOOLEAN', /** @ignore */ evaluate: function (r,w) { var left = this.leftSide.evaluate(r,w); var right = this.rightSide.evaluate(r,w); return Ember.isEqual(left, right); } }, '!=': { reservedWord: true, leftType: 'PRIMITIVE', rightType: 'PRIMITIVE', evalType: 'BOOLEAN', /** @ignore */ evaluate: function (r,w) { var left = this.leftSide.evaluate(r,w); var right = this.rightSide.evaluate(r,w); return !Ember.isEqual(left, right); } }, '<': { reservedWord: true, leftType: 'PRIMITIVE', rightType: 'PRIMITIVE', evalType: 'BOOLEAN', /** @ignore */ evaluate: function (r,w) { var left = this.leftSide.evaluate(r,w); var right = this.rightSide.evaluate(r,w); return Ember.compare(left, right) == -1; //left < right; } }, '<=': { reservedWord: true, leftType: 'PRIMITIVE', rightType: 'PRIMITIVE', evalType: 'BOOLEAN', /** @ignore */ evaluate: function (r,w) { var left = this.leftSide.evaluate(r,w); var right = this.rightSide.evaluate(r,w); return Ember.compare(left, right) != 1; //left <= right; } }, '>': { reservedWord: true, leftType: 'PRIMITIVE', rightType: 'PRIMITIVE', evalType: 'BOOLEAN', /** @ignore */ evaluate: function (r,w) { var left = this.leftSide.evaluate(r,w); var right = this.rightSide.evaluate(r,w); return Ember.compare(left, right) == 1; //left > right; } }, '>=': { reservedWord: true, leftType: 'PRIMITIVE', rightType: 'PRIMITIVE', evalType: 'BOOLEAN', /** @ignore */ evaluate: function (r,w) { var left = this.leftSide.evaluate(r,w); var right = this.rightSide.evaluate(r,w); return Ember.compare(left, right) != -1; //left >= right; } }, 'BEGINS_WITH': { reservedWord: true, leftType: 'PRIMITIVE', rightType: 'PRIMITIVE', evalType: 'BOOLEAN', /** @ignore */ evaluate: function (r,w) { var all = this.leftSide.evaluate(r,w); var start = this.rightSide.evaluate(r,w); return ( all && all.indexOf(start) === 0 ); } }, 'ENDS_WITH': { reservedWord: true, leftType: 'PRIMITIVE', rightType: 'PRIMITIVE', evalType: 'BOOLEAN', /** @ignore */ evaluate: function (r,w) { var all = this.leftSide.evaluate(r,w); var end = this.rightSide.evaluate(r,w); return ( all && all.indexOf(end) === (all.length - end.length) ); } }, 'CONTAINS': { reservedWord: true, leftType: 'PRIMITIVE', rightType: 'PRIMITIVE', evalType: 'BOOLEAN', /** @ignore */ evaluate: function (r,w) { var all = this.leftSide.evaluate(r,w) || []; var value = this.rightSide.evaluate(r,w); var allType = Ember.typeOf(all); if (allType === 'string') { return (all.indexOf(value) !== -1); } else if (allType === 'array' || all.toArray) { if (allType !== 'array') all = all.toArray(); var found = false; var i = 0; while ( found===false && i 0 ) return true; else return false; } function tokenIsMissingChilds (position) { var p = position; if ( p < 0 ) return true; return (expectedType('left',p) && !l[p].leftSide) || (expectedType('right',p) && !l[p].rightSide); } function typesAreMatching (parent, child) { var side = (child < parent) ? 'left' : 'right'; if ( parent < 0 || child < 0 ) return false; if ( !expectedType(side,parent) ) return false; if ( !evalType(child) ) return false; if ( expectedType(side,parent) == evalType(child) ) return true; else return false; } function preceedingTokenCanBeMadeChild (position) { var p = position; if ( !tokenIsMissingChilds(p) ) return false; if ( !preceedingTokenExists(p) ) return false; if ( typesAreMatching(p,p-1) ) return true; else return false; } function preceedingTokenCanBeMadeParent (position) { var p = position; if ( tokenIsMissingChilds(p) ) return false; if ( !preceedingTokenExists(p) ) return false; if ( !tokenIsMissingChilds(p-1) ) return false; if ( typesAreMatching(p-1,p) ) return true; else return false; } function makeChild (position) { var p = position; if (p<1) return false; l[p].leftSide = l[p-1]; removeToken(p-1); } function makeParent (position) { var p = position; if (p<1) return false; l[p-1].rightSide = l[p]; removeToken(p); } function removeParenthesesPair (position) { removeToken(position); removeToken(openParenthesisStack.pop()); } // step through the tokenList for (i=0; i < l.length; i++) { shouldCheckAgain = false; if ( l[i].tokenType == 'UNKNOWN' ) { error.push('found unknown token: '+l[i].tokenValue); } if ( l[i].tokenType == 'OPEN_PAREN' ) openParenthesisStack.push(i); if ( l[i].tokenType == 'CLOSE_PAREN' ) removeParenthesesPair(i); if ( preceedingTokenCanBeMadeChild(i) ) makeChild(i); if ( preceedingTokenCanBeMadeParent(i) ){ makeParent(i); shouldCheckAgain = true; } if ( shouldCheckAgain ) i--; } // error if tokenList l is not a single token now if (l.length == 1) l = l[0]; else error.push('string did not resolve to a single tree'); // error? if (error.length > 0) return {error: error.join(',\n'), tree: l}; // everything fine - token list is now a tree and can be returned else return l; }, // .......................................................... // ORDERING // /** Takes a string containing an order statement and returns an array describing this order for easier processing. Called by `parse()`. @param {String | Function} orderOp the string containing the order statement, or a comparison function @returns {Array | Function} array of order statement, or a function if a function was specified */ buildOrder: function (orderOp) { if (!orderOp) { return []; } else if (Ember.typeOf(orderOp) === 'function') { return orderOp; } else { var o = orderOp.split(','); for (var i=0; i < o.length; i++) { var p = o[i]; p = p.replace(/^\s+|\s+$/,''); p = p.replace(/\s+/,','); p = p.split(','); o[i] = {propertyName: p[0]}; if (p[1] && p[1] == 'DEEmber') o[i].descending = true; } return o; } } }); // Class Methods Ember.Query.reopenClass( /** @scope Ember.Query */ { /** Constant used for `Ember.Query#location` @type String */ LOCAL: 'local', /** Constant used for `Ember.Query#location` @type String */ REMOTE: 'remote', /** Given a query, returns the associated `storeKey`. For the inverse of this method see `Ember.Store.queryFor()`. @param {Ember.Query} query the query @returns {Number} a storeKey. */ storeKeyFor: function(query) { return query ? get(query, 'storeKey') : null; }, /** Will find which records match a give `Ember.Query` and return an array of store keys. This will also apply the sorting for the query. @param {Ember.Query} query to apply @param {Ember.RecordArray} records to search within @param {Ember.Store} store to materialize record from @returns {Array} array instance of store keys matching the Ember.Query (sorted) */ containsRecords: function(query, records, store) { var ret = []; for(var idx=0,len=get(records, 'length');idx record2, 0 if equal */ compareStoreKeys: function(query, store, storeKey1, storeKey2) { var record1 = store.materializeRecord(storeKey1), record2 = store.materializeRecord(storeKey2); return query.compare(record1, record2); }, /** Returns a `Ember.Query` instance reflecting the passed properties. Where possible this method will return cached query instances so that multiple calls to this method will return the same instance. This is not possible however, when you pass custom parameters or set ordering. All returned queries are frozen. Usually you will not call this method directly. Instead use the more convenient `Ember.Query.local()` and `Ember.Query.remote()`. Examples There are a number of different ways you can call this method. The following return local queries selecting all records of a particular type or types, including any subclasses: var people = Ember.Query.local(Ab.Person); var peopleAndCompanies = Ember.Query.local([Ab.Person, Ab.Company]); var people = Ember.Query.local('Ab.Person'); var peopleAndCompanies = Ember.Query.local('Ab.Person Ab.Company'.w()); var allRecords = Ember.Query.local(Ember.Record); The following will match a particular type of condition: var married = Ember.Query.local(Ab.Person, "isMarried=YES"); var married = Ember.Query.local(Ab.Person, "isMarried=%@", [YES]); var married = Ember.Query.local(Ab.Person, "isMarried={married}", { married: YES }); You can also pass a hash of options as the second parameter. This is how you specify an order, for example: var orderedPeople = Ember.Query.local(Ab.Person, { orderBy: "firstName" }); @param {String} location the query location. @param {Ember.Record|Array} recordType the record type or types. @param {String} conditions optional conditions @param {Hash} params optional params. or pass multiple args. @returns {Ember.Query} */ build: function(location, recordType, conditions, params) { var opts = null, ret, cache, key, tmp; // fast case for query objects. if (recordType && recordType.isQuery) { if (get(recordType, 'location') === location) { return recordType; } else { ret = recordType.copy(); set(ret, 'location', location); return ret.freeze(); } } // normalize recordType if (typeof recordType === 'string') { ret = getPath( recordType); if (!ret) throw "%@ did not resolve to a class".fmt(recordType); recordType = ret ; } else if (recordType && recordType.isEnumerable) { ret = []; recordType.forEach(function(t) { if (typeof t === 'string') t = getPath( t); if (!t) throw "cannot resolve record types: %@".fmt(recordType); ret.push(t); }, this); recordType = ret ; } else if (!recordType) recordType = Ember.Record; // find all records if (params === undefined) params = null; if (conditions === undefined) conditions = null; // normalize other params. if conditions is just a hash, treat as opts if (!params && (typeof conditions !== 'string')) { opts = conditions; conditions = null ; } // special case - easy to cache. if (!params && !opts) { tmp = Ember.Query._scq_recordTypeCache; if (!tmp) tmp = Ember.Query._scq_recordTypeCache = {}; cache = tmp[location]; if (!cache) cache = tmp[location] = {}; if (recordType.isEnumerable) { key = recordType.map(function(k) { return Ember.guidFor(k); }); key = key.sort().join(':'); } else key = Ember.guidFor(recordType); if (conditions) key = [key, conditions].join('::'); ret = cache[key]; if (!ret) { if (recordType.isEnumerable) { opts = { recordTypes: recordType.copy() }; } else opts = { recordType: recordType }; opts.location = location ; opts.conditions = conditions ; ret = cache[key] = Ember.Query.create(opts).freeze(); } // otherwise parse extra conditions and handle them } else { if (!opts) opts = {}; if (!opts.location) opts.location = location ; // allow override // pass one or more recordTypes. if (recordType && recordType.isEnumerable) { opts.recordsTypes = recordType; } else opts.recordType = recordType; // set conditions and params if needed if (conditions) opts.conditions = conditions; if (params) opts.parameters = params; ret = Ember.Query.create(opts).freeze(); } return ret ; }, /** Returns a `LOCAL` query with the passed options. For a full description of the parameters you can pass to this method, see `Ember.Query.build()`. @param {Ember.Record|Array} recordType the record type or types. @param {String} conditions optional conditions @param {Hash} params optional params. or pass multiple args. @returns {Ember.Query} */ local: function(recordType, conditions, params) { return this.build(Ember.Query.LOCAL, recordType, conditions, params); }, /** Returns a `REMOTE` query with the passed options. For a full description of the parameters you can pass to this method, see `Ember.Query.build()`. @param {Ember.Record|Array} recordType the record type or types. @param {String} conditions optional conditions @param {Hash} params optional params. or pass multiple args. @returns {Ember.Query} */ remote: function(recordType, conditions, params) { return this.build(Ember.Query.REMOTE, recordType, conditions, params); }, /** @private called by `Ember.Record.extend()`. invalidates `expandedRecordTypes` */ _scq_didDefineRecordType: function() { var q = Ember.Query._scq_queriesWithExpandedRecordTypes; if (q) { q.forEach(function(query) { Ember.propertyWillChange(query, 'expandedRecordTypes'); Ember.propertyDidChange(query, 'expandedRecordTypes'); }, this); q.clear(); } } }); /** @private Hash of registered comparisons by propery name. */ Ember.Query.comparisons = {}; /** Call to register a comparison for a specific property name. The function you pass should accept two values of this property and return -1 if the first is smaller than the second, 0 if they are equal and 1 if the first is greater than the second. @param {String} name of the record property @param {Function} custom comparison function @returns {Ember.Query} receiver */ Ember.Query.registerComparison = function(propertyName, comparison) { Ember.Query.comparisons[propertyName] = comparison; }; /** Call to register an extension for the query language. You shoud provide a name for your extension and a definition specifying how it should be parsed and evaluated. Have a look at `queryLanguage` for examples of definitions. TODO add better documentation here @param {String} tokenName name of the operator @param {Object} token extension definition @returns {Ember.Query} receiver */ Ember.Query.registerQueryExtension = function(tokenName, token) { get(Ember.Query, 'proto').queryLanguage[tokenName] = token; }; })({}); (function(exports) { // ========================================================================== // 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) // ========================================================================== // @global Ember var get = Ember.get, set = Ember.set; /** @class An error, used to represent an error state. Many API's within SproutCore will return an instance of this object whenever they have an error occur. An error includes an error code, description, and optional human readable label that indicates the item that failed. Depending on the error, other properties may also be added to the object to help you recover from the failure. You can pass error objects to various UI elements to display the error in the interface. You can easily determine if the value returned by some API is an error or not using the helper Ember.ok(value). Faking Error Objects --- You can actually make any object you want to be treated like an Error object by simply implementing two properties: isError and errorValue. If you set isError to YES, then calling Ember.ok(obj) on your object will return NO. If isError is YES, then Ember.val(obj) will return your errorValue property instead of the receiver. @extends Ember.Object @since SproutCore 1.0 */ Ember.StoreError = Ember.Object.extend( /** @scope Ember.StoreError.prototype */ { /** error code. Used to designate the error type. @type Number */ code: -1, /** Human readable description of the error. This can also be a non-localized key. @type String */ message: '', /** The value the error represents. This is used when wrapping a value inside of an error to represent the validation failure. @type Object */ errorValue: null, /** The original error object. Normally this will return the receiver. However, sometimes another object will masquarade as an error; this gives you a way to get at the underyling error. @type Ember.StoreError */ errorObject: function() { return this; }.property().cacheable(), /** Human readable name of the item with the error. @type String */ label: null, /** @private */ toString: function() { return "Ember.StoreError:%@:%@ (%@)".fmt(Ember.guidFor(this), get(this, 'message'), get(this, 'code')); }, /** Walk like a duck. @type Boolean */ isError: YES }) ; /** Creates a new Ember.StoreError instance with the passed description, label, and code. All parameters are optional. @param description {String} human readable description of the error @param label {String} human readable name of the item with the error @param code {Number} an error code to use for testing. @returns {Ember.StoreError} new error instance. */ Ember.StoreError.desc = function(description, label, value, code) { var opts = { message: description } ; if (label !== undefined) opts.label = label ; if (code !== undefined) opts.code = code ; if (value !== undefined) opts.errorValue = value ; return this.create(opts) ; } ; /** Shorthand form of the Ember.StoreError.desc method. @param description {String} human readable description of the error @param label {String} human readable name of the item with the error @param code {Number} an error code to use for testing. @returns {Ember.StoreError} new error instance. */ Ember.$error = function(description, label, value, c) { return Ember.StoreError.desc(description,label, value, c); } ; /** Returns NO if the passed value is an error object or false. @param {Object} ret object value @returns {Boolean} */ Ember.ok = function(ret) { return (ret !== false) && !(ret && ret.isError); }; /** @private */ Ember.$ok = Ember.ok; /** Returns the value of an object. If the passed object is an error, returns the value associated with the error; otherwise returns the receiver itself. @param {Object} obj the object @returns {Object} value */ Ember.val = function(obj) { if (obj && obj.isError) { return get(obj, 'errorValue') ; // Error has no value } else return obj ; }; /** @private */ Ember.$val = Ember.val; // STANDARD ERROR OBJECTS /** Standard error code for errors that do not support multiple values. @type Number */ Ember.StoreError.HAS_MULTIPLE_VALUES = -100 ; })({}); (function(exports) { // ========================================================================== // 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) // ========================================================================== var get = Ember.get, set = Ember.set, none = Ember.none, copy = Ember.copy, K; /** @class A Record is the core model class in SproutCore. It is analogous to NSManagedObject in Core Data and EOEnterpriseObject in the Enterprise Objects Framework (aka WebObjects), or ActiveRecord::Base in Rails. To create a new model class, in your SproutCore workspace, do: $ sc-gen model MyApp.MyModel This will create MyApp.MyModel in clients/my_app/models/my_model.js. The core attributes hash is used to store the values of a record in a format that can be easily passed to/from the server. The values should generally be stored in their raw string form. References to external records should be stored as primary keys. Normally you do not need to work with the attributes hash directly. Instead you should use get/set on normal record properties. If the property is not defined on the object, then the record will check the attributes hash instead. You can bulk update attributes from the server using the `updateAttributes()` method. @extends Ember.Object @see Ember.RecordAttribute @since SproutCore 1.0 */ Ember.Record = Ember.Object.extend( /** @scope Ember.Record.prototype */ { /** Deprecated. Use instanceof keyword instead. @deprecated @type Boolean @default YES */ isRecord: YES, /** If you have nested records @type Boolean @default NO */ isParentRecord: NO, // ............................... // PROPERTIES // /** This is the primary key used to distinguish records. If the keys match, the records are assumed to be identical. @type String @default 'guid' */ primaryKey: 'guid', /** Returns the id for the record instance. The id is used to uniquely identify this record instance from all others of the same type. If you have a `primaryKey set on this class, then the id will be the value of the `primaryKey` property on the underlying JSON hash. @type String @property @dependsOn storeKey */ id: function(key, value) { if (value !== undefined) { this.writeAttribute(get(this, 'primaryKey'), value); return value; } else { return Ember.Store.idFor(get(this, 'storeKey')); } }.property('storeKey').cacheable(), /** All records generally have a life cycle as they are created or loaded into memory, modified, committed and finally destroyed. This life cycle is managed by the status property on your record. The status of a record is modelled as a finite state machine. Based on the current state of the record, you can determine which operations are currently allowed on the record and which are not. In general, a record can be in one of five primary states: `Ember.Record.EMPTY`, `Ember.Record.BUSY`, `Ember.Record.READY`, `Ember.Record.DESTROYED`, `Ember.Record.ERROR`. These are all described in more detail in the class mixin (below) where they are defined. @type Number @property @dependsOn storeKey */ status: function() { return this.store.readStatus(get(this, 'storeKey')); }.property('storeKey').cacheable(), /** The store that owns this record. All changes will be buffered into this store and committed to the rest of the store chain through here. This property is set when the record instance is created and should not be changed or else it will break the record behavior. @type Ember.Store @default null */ store: null, /** This is the store key for the record, it is used to link it back to the dataHash. If a record is reused, this value will be replaced. You should not edit this store key but you may sometimes need to refer to this store key when implementing a Server object. @type Number @default null */ storeKey: null, /** YES when the record has been destroyed @type Boolean @property @dependsOn status */ isDestroyed: function() { return !!(get(this, 'status') & Ember.Record.DESTROYED); }.property('status').cacheable(), /** `YES` when the record is in an editable state. You can use this property to quickly determine whether attempting to modify the record would raise an exception. This property is both readable and writable. Note however that if you set this property to `YES` but the status of the record is anything but `Ember.Record.READY`, the return value of this property may remain `NO`. @type Boolean @property @dependsOn status */ isEditable: function(key, value) { if (value !== undefined) this._screc_isEditable = value; return (get(this, 'status') & Ember.Record.READY) && this._screc_isEditable; }.property('status').cacheable(), /** @private Backing value for isEditable */ _screc_isEditable: YES, // default /** `YES` when the record's contents have been loaded for the first time. You can use this to quickly determine if the record is ready to display. @type Boolean @property @dependsOn status */ isLoaded: function() { var status = get(this, 'status'); return !((status===K.EMPTY) || (status===K.BUSY_LOADING) || (status===K.ERROR)); }.property('status').cacheable(), /** If set, this should be an array of active relationship objects that need to be notified whenever the underlying record properties change. Currently this is only used by toMany relationships, but you could possibly patch into this yourself also if you are building your own relationships. @type Array @default null */ relationships: null, /** This will return the raw attributes that you can edit directly. If you make changes to this hash, be sure to call `beginEditing()` before you get the attributes and `endEditing()` afterwards. @type Hash @property **/ attributes: function() { return get(this, 'store').readEditableDataHash(get(this, 'storeKey')); }.property(), /** This will return the raw attributes that you cannot edit directly. It is useful if you want to efficiently look at multiple attributes in bulk. If you would like to edit the attributes, see the `attributes` property instead. @type Hash @property **/ readOnlyAttributes: function() { var ret = get(this, 'store').readDataHash(get(this, 'storeKey')); return ret ? copy(ret) : null; }.property(), /** The namespace which to retrieve the childRecord Types from @type String @default null */ nestedRecordNamespace: null, /** Whether or not this is a nested Record. @type Boolean @property */ isNestedRecord: function(){ var store = get(this, 'store'), sk = get(this, 'storeKey'); return !!store.parentStoreKeyExists(sk); }.property('storeKey').cacheable(), /** The parent record if this is a nested record. @type Boolean @property */ parentRecord: function(){ var sk = get(this, 'storeKey'), store = get(this, 'store'); return store.materializeParentRecord(sk); }.property('storeKey').cacheable(), // ............................... // CRUD OPERATIONS // /** Refresh the record from the persistent store. If the record was loaded from a persistent store, then the store will be asked to reload the record data from the server. If the record is new and exists only in memory then this call will have no effect. @param {boolean} recordOnly optional param if you want to only this record even if it is a child record. @param {Function} callback optional callback that will fire when request finishes @returns {Ember.Record} receiver */ refresh: function(recordOnly, callback) { var store = get(this, 'store'), rec, ro, sk = get(this, 'storeKey'), prKey = store.parentStoreKeyExists(); if (!callback && 'function'===typeof recordOnly) { callback = recordOnly; recordOnly = false; } // If we only want to refresh this record or it doesn't have a parent // record we will commit this record if (recordOnly || (none(recordOnly) && none(prKey))) { store.refreshRecord(null, null, sk, callback); } else if (prKey) { rec = store.materializeRecord(prKey); rec.refresh(false, callback); } return this ; }, /** Deletes the record along with any dependent records. This will mark the records destroyed in the store as well as changing the isDestroyed property on the record to YES. If this is a new record, this will avoid creating the record in the first place. @param {boolean} recordOnly optional param if you want to only THIS record even if it is a child record. @returns {Ember.Record} receiver */ destroy: function(recordOnly) { var store = get(this, 'store'), rec, ro, sk = get(this, 'storeKey'), prKey = store.parentStoreKeyExists(); // If we only want to destroy this record or it doesn't have a parent // record we will commit this record ro = recordOnly || (none(recordOnly) && none(prKey)); if (ro){ Ember.propertyWillChange(this, 'status'); store.destroyRecord(null, null, sk); Ember.propertyDidChange(this, 'status'); // If there are any aggregate records, we might need to propagate our // new status to them. this.propagateToAggregates(); } else if (prKey){ rec = store.materializeRecord(prKey); rec.destroy(false); } return this ; }, /** You can invoke this method anytime you need to make the record as dirty. This will cause the record to be commited when you `commitChanges()` on the underlying store. If you use the `writeAttribute()` primitive, this method will be called for you. If you pass the key that changed it will ensure that observers are fired only once for the changed property instead of `allPropertiesDidChange()` @param {String} key key that changed (optional) @returns {Ember.Record} receiver */ recordDidChange: function(key) { // If we have a parent, they changed too! var p = get(this, 'parentRecord'); if (p) p.recordDidChange(); get(this, 'store').recordDidChange(null, null, get(this, 'storeKey'), key); this.notifyPropertyChange('status'); // If there are any aggregate records, we might need to propagate our new // status to them. this.propagateToAggregates(); return this ; }, // ............................... // ATTRIBUTES // /** @private Current edit level. Used to defer editing changes. */ _editLevel: 0 , /** Defers notification of record changes until you call a matching `endEditing()` method. This method is called automatically whenever you set an attribute, but you can call it yourself to group multiple changes. Calls to `beginEditing()` and `endEditing()` can be nested. @returns {Ember.Record} receiver */ beginEditing: function() { this._editLevel++; return this ; }, /** Notifies the store of record changes if this matches a top level call to `beginEditing()`. This method is called automatically whenever you set an attribute, but you can call it yourself to group multiple changes. Calls to `beginEditing()` and `endEditing()` can be nested. @param {String} key key that changed (optional) @returns {Ember.Record} receiver */ endEditing: function(key) { if(--this._editLevel <= 0) { this._editLevel = 0; this.recordDidChange(key); } return this ; }, /** Reads the raw attribute from the underlying data hash. This method does not transform the underlying attribute at all. @param {String} key the attribute you want to read @returns {Object} the value of the key, or undefined if it doesn't exist */ readAttribute: function(key) { var store = get(this, 'store'), storeKey = get(this, 'storeKey'); var attrs = store.readDataHash(storeKey); return attrs ? attrs[key] : undefined ; }, /** Updates the passed attribute with the new value. This method does not transform the value at all. If instead you want to modify an array or hash already defined on the underlying json, you should instead get an editable version of the attribute using `editableAttribute()`. @param {String} key the attribute you want to read @param {Object} value the value you want to write @param {Boolean} ignoreDidChange only set if you do NOT want to flag record as dirty @returns {Ember.Record} receiver */ writeAttribute: function(key, value, ignoreDidChange) { var store = get(this, 'store'), storeKey = get(this, 'storeKey'), attrs; attrs = store.readEditableDataHash(storeKey); if (!attrs) throw K.BAD_STATE_ERROR; // if value is the same, do not flag record as dirty if (value !== attrs[key]) { if(!ignoreDidChange) this.beginEditing(); attrs[key] = value; // If the key is the primaryKey of the record, we need to tell the store // about the change. if (key===get(this, 'primaryKey')) { Ember.propertyWillChange(this, 'id'); // Reset computed value Ember.Store.replaceIdFor(storeKey, value) ; Ember.propertyDidChange(this, 'id'); // Reset computed value } if(!ignoreDidChange) this.endEditing(key); } return this ; }, /** This will also ensure that any aggregate records are also marked dirty if this record changes. Should not have to be called manually. */ propagateToAggregates: function() { var storeKey = get(this, 'storeKey'), recordType = Ember.Store.recordTypeFor(storeKey), idx, len, key, val, recs, aggregates; aggregates = recordType.aggregates; // if recordType aggregates are not set up yet, make sure to // create the cache first if (!aggregates) { var dataHash = get(this, 'store').readDataHash(storeKey); var attrFor = Ember.RecordAttribute.attrFor; var attr; aggregates = []; for(var k in dataHash) { attr = attrFor(this, k); if (attr && get(attr, 'aggregate')) aggregates.push(k); } recordType.aggregates = aggregates; } // now loop through all aggregate properties and mark their related // record objects as dirty var K = Ember.Record, dirty = K.DIRTY, readyNew = K.READY_NEW, destroyed = K.DESTROYED, readyClean = K.READY_CLEAN, iter; /** @private If the child is dirty, then make sure the parent gets a dirty status. (If the child is created or destroyed, there's no need, because the parent will dirty itself when it modifies that relationship.) @param {Ember.Record} record to propagate to */ iter = function(rec) { var childStatus, parentStatus; if (rec) { childStatus = get(this, 'status'); if ((childStatus & dirty) || (childStatus & readyNew) || (childStatus & destroyed)) { parentStatus = get(rec, 'status'); if (parentStatus === readyClean) { // Note: storeDidChangeProperties() won't put it in the // changelog! get(rec, 'store').recordDidChange(get(rec, 'constructor'), null, get(rec, 'storeKey'), null, YES); } } } }; for(idx=0,len=aggregates.length;idx=0) manyArrays[loc].recordPropertyDidChange(keys); } }, /** Normalizing a record will ensure that the underlying hash conforms to the record attributes such as their types (transforms) and default values. This method will write the conforming hash to the store and return the materialized record. By normalizing the record, you can use the `attributes` property and be assured that it will conform to the defined model. For example, this can be useful in the case where you need to send a JSON representation to some server after you have used `.createRecord()`, since this method will enforce the 'rules' in the model such as their types and default values. You can also include null values in the hash with the includeNull argument. @param {Boolean} includeNull will write empty (null) attributes @returns {Ember.Record} the normalized record */ normalize: function(includeNull) { var primaryKey = this.primaryKey, recordId = get(this, 'id'), store = get(this, 'store'), storeKey = get(this, 'storeKey'), key, typeClass, recHash, attrValue, normChild, isRecord, isChild, defaultVal, keyForDataHash, attr; var dataHash = store.readEditableDataHash(storeKey) || {}; dataHash[primaryKey] = recordId; recHash = store.readDataHash(storeKey); var attrFor = Ember.RecordAttribute.attrFor; for (key in this) { // make sure property is a record attribute. attr = attrFor(this, key); if (attr) { keyForDataHash = get(attr, 'key') || key; // handle alt keys typeClass = get(attr, 'typeClass'); isRecord = Ember.typeOf(get(attr, 'typeClass')) === 'class'; isChild = get(attr, 'isNestedRecordTransform'); if (isRecord) { attrValue = recHash[keyForDataHash]; if (attrValue !== undefined) { // write value already there dataHash[keyForDataHash] = attrValue; } else { // or write default defaultVal = get(attr, 'defaultValue'); // computed default value if ('function' === typeof defaultVal) { dataHash[keyForDataHash] = defaultVal(this, key, defaultVal); } else { // plain value dataHash[keyForDataHash] = defaultVal; } } } else if (isChild) { attrValue = get(this, key); // Sometimes a child attribute property does not refer to a // child record. Catch this and don't try to normalize. if (attrValue && attrValue.normalize) { attrValue.normalize(); } } else { attrValue = get(this, key); if (attrValue!==undefined || (attrValue===null && includeNull)) { attrValue = attr.fromType(this, key, attrValue); dataHash[keyForDataHash] = attrValue; } } } } return this; }, setUnknownProperty: function(key, value) { // If the value is undefined, it means it has not been set to null // by Ember.Record (and thus reserved as an internal property). // // Since we will always circumvent the normal set() semantics in // this case, the value will *never* be set, so every call to // `set` with this key will go through `unknownProperty` and proxy // the change to the data hash. if (this[key] === undefined) { // first check if we should ignore unknown properties for this // recordType var storeKey = get(this, 'storeKey'), recordType = Ember.Store.recordTypeFor(storeKey); if(recordType.ignoreUnknownProperties===YES) { this[key] = value; return value; } // if we're modifying the PKEY, then `Ember.Store` needs to relocate where // this record is cached. store the old key, update the value, then let // the store do the housekeeping... var primaryKey = get(this, 'primaryKey'); this.writeAttribute(key,value); // update ID if needed if (key === primaryKey) { Ember.Store.replaceIdFor(storeKey, value); } return this.unknownProperty(key); } else { // This is an internal reserved property. Do the normal `set` behavior. return this._super(key, value); } }, /** If you try to get/set a property not defined by the record, then this method will be called. It will try to get the value from the set of attributes. This will also check is `ignoreUnknownProperties` is set on the recordType so that they will not be written to `dataHash` unless explicitly defined in the model schema. @param {String} key the attribute being get/set @param {Object} value the value to set the key to, if present @returns {Object} the value */ unknownProperty: function(key) { return this.readAttribute(key); }, /** Lets you commit this specific record to the store which will trigger the appropriate methods in the data source for you. @param {Hash} params optional additonal params that will passed down to the data source @param {boolean} recordOnly optional param if you want to only commit a single record if it has a parent. @param {Function} callback optional callback that the store will fire once the datasource finished committing @returns {Ember.Record} receiver */ commitRecord: function(params, recordOnly, callback) { var store = get(this, 'store'), rec, ro, sk = get(this, 'storeKey'), prKey = store.parentStoreKeyExists(); // If we only want to commit this record or it doesn't have a parent record // we will commit this record ro = recordOnly || (Ember.none(recordOnly) && Ember.none(prKey)); if (ro){ store.commitRecord(undefined, undefined, get(this, 'storeKey'), params, callback); } else if (prKey){ rec = store.materializeRecord(prKey); rec.commitRecord(params, recordOnly, callback); } return this ; }, // .......................................................... // EMULATE Ember.StoreError API // /** Returns `YES` whenever the status is Ember.Record.ERROR. This will allow you to put the UI into an error state. @type Boolean @property @dependsOn status */ isError: function() { return get(this, 'status') & Ember.Record.ERROR; }.property('status').cacheable(), /** Returns the receiver if the record is in an error state. Returns null otherwise. @type Ember.Record @property @dependsOn isError */ errorValue: function() { return get(this, 'isError') ? Ember.val(get(this, 'errorObject')) : null ; }.property('isError').cacheable(), /** Returns the current error object only if the record is in an error state. If no explicit error object has been set, returns Ember.Record.GENERIC_ERROR. @type Ember.StoreError @property @dependsOn isError */ errorObject: function() { if (get(this, 'isError')) { var store = get(this, 'store'); return store.readError(get(this, 'storeKey')) || K.GENERIC_ERROR; } else return null ; }.property('isError').cacheable(), // ............................... // PRIVATE // /** @private Sets the key equal to value. This version will first check to see if the property is an `Ember.RecordAttribute`, and if so, will ensure that its isEditable property is `YES` before attempting to change the value. @param key {String} the property to set @param value {Object} the value to set or null. @returns {Ember.Record} */ set: function(key, value) { var func = this[key]; if (func && func.isProperty && !get(func, 'isEditable')) { return this; } return this._super(key, value); }, /** @private Creates string representation of record, with status. @returns {String} */ toString: function() { // We won't use 'readOnlyAttributes' here because accessing them directly // avoids a Ember.copy() -- we'll be careful not to edit anything. var attrs = get(this, 'store').readDataHash(get(this, 'storeKey')); return "%@(%@) %@".fmt(this.constructor.toString(), Ember.inspect(attrs), this.statusString()); }, /** @private Creates string representation of record, with status. @returns {String} */ statusString: function() { var ret = [], status = get(this, 'status'); for(var prop in Ember.Record) { if(prop.match(/[A-Z_]$/) && Ember.Record[prop]===status) { ret.push(prop); } } return ret.join(" "); }, /** Registers a child record with this parent record. If the parent already knows about the child record, return the cached instance. If not, create the child record instance and add it to the child record cache. @param {Hash} value The hash of attributes to apply to the child record. @param {Integer} key The store key that we are asking for @param {String} path The property path of the child record @returns {Ember.Record} the child record that was registered */ registerNestedRecord: function(value, key, path) { var store, psk, csk, childRecord, recordType; // if no path is entered it must be the key if (Ember.none(path)) path = key; // if a record instance is passed, simply use the storeKey. This allows // you to pass a record from a chained store to get the same record in the // current store. if (value instanceof Ember.Record) { childRecord = value; } else { recordType = this._materializeNestedRecordType(value, key); childRecord = this.createNestedRecord(recordType, value); } if (childRecord){ set(this, 'isParentRecord', YES); store = get(this, 'store'); psk = get(this, 'storeKey'); csk = get(childRecord, 'storeKey'); store.registerChildToParent(psk, csk, path); } return childRecord; }, /** @private private method that retrieves the `recordType` from the hash that is provided. Important for use in polymorphism but you must have the following items in the parent record: `nestedRecordNamespace` <= this is the object that has the `Ember.Records` defined @param {Hash} value The hash of attributes to apply to the child record. @param {String} key the name of the key on the attribute @param {Ember.Record} the record that was materialized */ _materializeNestedRecordType: function(value, key){ var childNS, recordType, ret, attr, t; // Get the record type, first checking the "type" property on the hash. t = Ember.typeOf(value); if (t === 'instance' || t === 'object') { // Get the record type. childNS = get(this, 'nestedRecordNamespace'); if (get(value, 'type') && !Ember.none(childNS)) { recordType = get(childNS, get(value, 'type')); } } // Maybe it's not a hash or there was no type property. if (!recordType && key) { attr = Ember.RecordAttribute.attrFor(this, key); if (attr) recordType = get(attr, 'typeClass'); } // When all else fails throw and exception. if (!Ember.Record.detect(recordType)) { throw 'Ember.Child: Error during transform: Invalid record type.'; } return recordType; }, /** Creates a new nested record instance. @param {Ember.Record} recordType The type of the nested record to create. @param {Hash} hash The hash of attributes to apply to the child record. (may be null) @returns {Ember.Record} the nested record created */ createNestedRecord: function(recordType, hash) { var store, id, sk, pk, cr = null, existingId = null; Ember.run(this, function() { hash = hash || {}; // init if needed existingId = hash[get(recordType, 'proto').primaryKey]; store = get(this, 'store'); if (Ember.none(store)) throw 'Error: during the creation of a child record: NO STORE ON PARENT!'; if (!id && (pk = get(recordType, 'proto').primaryKey)) { id = hash[pk]; // In case there isnt a primary key supplied then we create on // on the fly sk = id ? store.storeKeyExists(recordType, id) : null; if (sk){ store.writeDataHash(sk, hash); cr = store.materializeRecord(sk); } else { cr = store.createRecord(recordType, hash) ; if (Ember.none(id)){ sk = get(cr, 'storeKey'); id = 'cr'+sk; Ember.Store.replaceIdFor(sk, id); hash = store.readEditableDataHash(sk); hash[pk] = id; } } } // ID processing if necessary if (Ember.none(existingId) && this.generateIdForChild) this.generateIdForChild(cr); }); return cr; }, _nestedRecordKey: 0, /** Override this function if you want to have a special way of creating ids for your child records @param {Ember.Record} childRecord @returns {String} the id generated */ generateIdForChild: function(childRecord){} }) ; // Class Methods Ember.Record.reopenClass( /** @scope Ember.Record.prototype */ { /** Whether to ignore unknown properties when they are being set on the record object. This is useful if you want to strictly enforce the model schema and not allow dynamically expanding it by setting new unknown properties @static @type Boolean @default NO */ ignoreUnknownProperties: NO, // .......................................................... // CONSTANTS // /** Generic state for records with no local changes. Use a logical AND (single `&`) to test record status @static @constant @type Number @default 0x0001 */ CLEAN: 0x0001, // 1 /** Generic state for records with local changes. Use a logical AND (single `&`) to test record status @static @constant @type Number @default 0x0002 */ DIRTY: 0x0002, // 2 /** State for records that are still loaded. A record instance should never be in this state. You will only run into it when working with the low-level data hash API on `Ember.Store`. Use a logical AND (single `&`) to test record status @static @constant @type Number @default 0x0100 */ EMPTY: 0x0100, // 256 /** State for records in an error state. Use a logical AND (single `&`) to test record status @static @constant @type Number @default 0x1000 */ ERROR: 0x1000, // 4096 /** Generic state for records that are loaded and ready for use Use a logical AND (single `&`) to test record status @static @constant @type Number @default 0x0200 */ READY: 0x0200, // 512 /** State for records that are loaded and ready for use with no local changes Use a logical AND (single `&`) to test record status @static @constant @type Number @default 0x0201 */ READY_CLEAN: 0x0201, // 513 /** State for records that are loaded and ready for use with local changes Use a logical AND (single `&`) to test record status @static @constant @type Number @default 0x0202 */ READY_DIRTY: 0x0202, // 514 /** State for records that are new - not yet committed to server Use a logical AND (single `&`) to test record status @static @constant @type Number @default 0x0203 */ READY_NEW: 0x0203, // 515 /** Generic state for records that have been destroyed Use a logical AND (single `&`) to test record status @static @constant @type Number @default 0x0400 */ DESTROYED: 0x0400, // 1024 /** State for records that have been destroyed and committed to server Use a logical AND (single `&`) to test record status @static @constant @type Number @default 0x0401 */ DESTROYED_CLEAN: 0x0401, // 1025 /** State for records that have been destroyed but not yet committed to server Use a logical AND (single `&`) to test record status @static @constant @type Number @default 0x0402 */ DESTROYED_DIRTY: 0x0402, // 1026 /** Generic state for records that have been submitted to data source Use a logical AND (single `&`) to test record status @static @constant @type Number @default 0x0800 */ BUSY: 0x0800, // 2048 /** State for records that are still loading data from the server Use a logical AND (single `&`) to test record status @static @constant @type Number @default 0x0804 */ BUSY_LOADING: 0x0804, // 2052 /** State for new records that were created and submitted to the server; waiting on response from server Use a logical AND (single `&`) to test record status @static @constant @type Number @default 0x0808 */ BUSY_CREATING: 0x0808, // 2056 /** State for records that have been modified and submitted to server Use a logical AND (single `&`) to test record status @static @constant @type Number @default 0x0810 */ BUSY_COMMITTING: 0x0810, // 2064 /** State for records that have requested a refresh from the server. Use a logical AND (single `&`) to test record status. @static @constant @type Number @default 0x0820 */ BUSY_REFRESH: 0x0820, // 2080 /** State for records that have requested a refresh from the server. Use a logical AND (single `&`) to test record status @static @constant @type Number @default 0x0821 */ BUSY_REFRESH_CLEAN: 0x0821, // 2081 /** State for records that have requested a refresh from the server. Use a logical AND (single `&`) to test record status @static @constant @type Number @default 0x0822 */ BUSY_REFRESH_DIRTY: 0x0822, // 2082 /** State for records that have been destroyed and submitted to server Use a logical AND (single `&`) to test record status @static @constant @type Number @default 0x0840 */ BUSY_DESTROYING: 0x0840, // 2112 // .......................................................... // ERRORS // /** Error for when you try to modify a record while it is in a bad state. @static @constant @type Ember.StoreError */ BAD_STATE_ERROR: Ember.$error("Internal Inconsistency"), /** Error for when you try to create a new record that already exists. @static @constant @type Ember.StoreError */ RECORD_EXISTS_ERROR: Ember.$error("Record Exists"), /** Error for when you attempt to locate a record that is not found @static @constant @type Ember.StoreError */ NOT_FOUND_ERROR: Ember.$error("Not found "), /** Error for when you try to modify a record that is currently busy @static @constant @type Ember.StoreError */ BUSY_ERROR: Ember.$error("Busy"), /** Generic unknown record error @static @constant @type Ember.StoreError */ GENERIC_ERROR: Ember.$error("Generic Error"), /** @private The next child key to allocate. A nextChildKey must always be greater than 0. */ _nextChildKey: 0, // .......................................................... // CLASS METHODS // /** Helper method returns a new `Ember.RecordAttribute` instance to map a simple value or to-one relationship and then defines it as a computed property. At the very least, you should pass the type class you expect the attribute to have. You may pass any additional options as well. Use this helper when you define Ember.Record subclasses. MyApp.Contact = Ember.Record.extend({ firstName: Ember.Record.attr(String, { isRequired: YES }) }); @param {Class} type the attribute type @param {Hash} opts the options for the attribute @returns {Ember.RecordAttribute} created instance */ attr: function(type, opts) { return Ember.RecordAttribute.attr(type, opts).computed(); }, /** Returns an `Ember.RecordAttribute` that describes a fetched attribute. When you reference this attribute, it will return an `Ember.RecordArray` that uses the type as the fetch key and passes the attribute value as a param. Use this helper when you define Ember.Record subclasses. MyApp.Group = Ember.Record.extend({ contacts: Ember.Record.fetch('MyApp.Contact') }); @param {Ember.Record|String} recordType The type of records to load @param {Hash} opts the options for the attribute @returns {Ember.RecordAttribute} created instance */ fetch: function(recordType, opts) { return Ember.FetchedAttribute.attr(recordType, opts).computed(); }, /** Will return one of the following: 1. `Ember.ManyAttribute` that describes a record array backed by an array of guids stored in the underlying JSON. 2. `Ember.ChildrenAttribute` that describes a record array backed by a array of hashes. You can edit the contents of this relationship. For `Ember.ManyAttribute`, If you set the inverse and `isMaster: NO` key, then editing this array will modify the underlying data, but the inverse key on the matching record will also be edited and that record will be marked as needing a change. @param {Ember.Record|String} recordType The type of record to create @param {Hash} opts the options for the attribute @returns {Ember.ManyAttribute|Ember.ChildrenAttribute} created instance */ toMany: function(recordType, opts) { opts = opts || {}; var isNested = opts.nested || opts.isNested; var attr; if(isNested){ attr = Ember.ChildrenAttribute.attr(recordType, opts); } else { attr = Ember.ManyAttribute.attr(recordType, opts); } return attr.computed(); }, /** Will return one of the following: 1. `Ember.SingleAttribute` that converts the underlying ID to a single record. If you modify this property, it will rewrite the underyling ID. It will also modify the inverse of the relationship, if you set it. 2. `Ember.ChildAttribute` that you can edit the contents of this relationship. @param {Ember.Record|String} recordType the type of the record to create @param {Hash} opts additional options @returns {Ember.SingleAttribute|Ember.ChildAttribute} created instance */ toOne: function(recordType, opts) { opts = opts || {}; var isNested = opts.nested || opts.isNested; var attr; if(isNested){ attr = Ember.ChildAttribute.attr(recordType, opts); } else { attr = Ember.SingleAttribute.attr(recordType, opts); } return attr.computed(); }, /** Returns all storeKeys mapped by Id for this record type. This method is used mostly by the `Ember.Store` and the Record to coordinate. You will rarely need to call this method yourself. @returns {Hash} */ storeKeysById: function() { var key = 'storeKey-'+Ember.guidFor(this), ret = this[key]; if (!ret) ret = this[key] = {}; return ret; }, /** Given a primaryKey value for the record, returns the associated storeKey. If the primaryKey has not been assigned a storeKey yet, it will be added. For the inverse of this method see `Ember.Store.idFor()` and `Ember.Store.recordTypeFor()`. @param {String} id a record id @returns {Number} a storeKey. */ storeKeyFor: function(id) { var storeKeys = this.storeKeysById(), ret = storeKeys[id]; if (!ret) { ret = Ember.Store.generateStoreKey(); Ember.Store.idsByStoreKey[ret] = id ; Ember.Store.recordTypesByStoreKey[ret] = this ; storeKeys[id] = ret ; } return ret ; }, /** 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 {String} id a record id @returns {Number} a storeKey. */ storeKeyExists: function(id) { var storeKeys = this.storeKeysById(), ret = storeKeys[id]; return ret ; }, /** Returns a record with the named ID in store. @param {Ember.Store} store the store @param {String} id the record id or a query @returns {Ember.Record} record instance */ find: function(store, id) { return store.find(this, id); }, /** @private - enhance extend to notify Ember.Query as well. */ extend: function() { var ret = Ember.Object.extend.apply(this, arguments); // Clear aggregates cache when creating a new subclass // of Ember.Record ret.aggregates = null; Ember.Query._scq_didDefineRecordType(ret); return ret ; } }) ; K = Ember.Record; })({}); (function(exports) { // ========================================================================== // 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) // ========================================================================== var get = Ember.get, set = Ember.set, getPath = Ember.getPath; /** @class A RecordAttribute describes a single attribute on a record. It is used to generate computed properties on records that can automatically convert data types and verify data. When defining an attribute on an Ember.Record, you can configure it this way: title: Ember.Record.attr(String, { defaultValue: 'Untitled', isRequired: YES|NO }) In addition to having predefined transform types, there is also a way to set a computed relationship on an attribute. A typical example of this would be if you have record with a parentGuid attribute, but are not able to determine which record type to map to before looking at the guid (or any other attributes). To set up such a computed property, you can attach a function in the attribute definition of the Ember.Record subclass: relatedToComputed: Ember.Record.toOne(function() { return (this.readAttribute('relatedToComputed').indexOf("foo")==0) ? MyApp.Foo : MyApp.Bar; }) Notice that we are not using get() to avoid another transform which would trigger an infinite loop. You usually will not work with RecordAttribute objects directly, though you may extend the class in any way that you like to create a custom attribute. A number of default RecordAttribute types are defined on the Ember.Record. @extends Ember.Object @see Ember.Record @see Ember.ManyAttribute @see Ember.SingleAttribute @since SproutCore 1.0 */ Ember.RecordAttribute = Ember.Object.extend( /** @scope Ember.RecordAttribute.prototype */ { /** Walk like a duck. @type Boolean @default YES */ isRecordAttribute: YES, /** The default value. If attribute is `null` or `undefined`, this default value will be substituted instead. Note that `defaultValue`s are not converted, so the value should be in the output type expected by the attribute. If you use a `defaultValue` function, the arguments given to it are the record instance and the key. @type Object|function @default null */ defaultValue: null, /** The attribute type. Must be either an object class or a property path naming a class. The built in handler allows all native types to pass through, converts records to ids and dates to UTF strings. If you use the `attr()` helper method to create a RecordAttribute instance, it will set this property to the first parameter you pass. @type Object|String @default String */ type: String, /** The underlying attribute key name this attribute should manage. If this property is left empty, then the key will be whatever property name this attribute assigned to on the record. If you need to provide some kind of alternate mapping, this provides you a way to override it. @type String @default null */ key: null, /** If `YES`, then the attribute is required and will fail validation unless the property is set to a non-null or undefined value. @type Boolean @default NO */ isRequired: NO, /** If `NO` then attempts to edit the attribute will be ignored. @type Boolean @default YES */ isEditable: YES, /** If set when using the Date format, expect the ISO8601 date format. This is the default. @type Boolean @default YES */ useIsoDate: YES, /** Can only be used for toOne or toMany relationship attributes. If YES, this flag will ensure that any related objects will also be marked dirty when this record dirtied. Useful when you might have multiple related objects that you want to consider in an 'aggregated' state. For instance, by changing a child object (image) you might also want to automatically mark the parent (album) dirty as well. @type Boolean @default NO */ aggregate: NO, // .......................................................... // HELPER PROPERTIES // /** Returns the type, resolved to a class. If the type property is a regular class, returns the type unchanged. Otherwise attempts to lookup the type as a property path. @property @type Object @default String */ typeClass: function() { var ret = get(this, 'type'); if (Ember.typeOf(ret) === 'string') ret = getPath(ret); return ret ; }.property('type').cacheable(), /** Finds the transform handler. Attempts to find a transform that you registered using registerTransform for this attribute's type, otherwise defaults to using the default transform for String. @property @type Transform */ transform: function() { var klass = get(this, 'typeClass') || String, transforms = Ember.RecordAttribute.transforms, ret ; // walk up class hierarchy looking for a transform handler while(klass && !(ret = transforms[Ember.guidFor(klass)])) { // check if super has create property to detect Ember.Object's if(klass.superclass && klass.superclass.hasOwnProperty('create')) { klass = klass.superclass ; } // otherwise return the function transform handler else klass = 'function' ; } return ret ; }.property('typeClass').cacheable(), // .......................................................... // LOW-LEVEL METHODS // /** Converts the passed value into the core attribute value. This will apply any format transforms. You can install standard transforms by adding to the `Ember.RecordAttribute.transforms` hash. See Ember.RecordAttribute.registerTransform() for more. @param {Ember.Record} record The record instance @param {String} key The key used to access this attribute on the record @param {Object} value The property value before being transformed @returns {Object} The transformed value */ toType: function(record, key, value) { var transform = get(this, 'transform'), type = get(this, 'typeClass'), children; if (transform && transform.to) { value = transform.to(value, this, type, record, key) ; // if the transform needs to do something when its children change, we need to set up an observer for it if(!Ember.none(value) && (children = transform.observesChildren)) { var i, len = children.length, // store the record, transform, and key so the observer knows where it was called from context = { record: record, key: key }; for(i = 0; i < len; i++) Ember.addObserver(value, children[i], this, this._EmberRA_childObserver, context); } } return value ; }, /** @private Shared observer used by any attribute whose transform creates a seperate object that needs to write back to the datahash when it changes. For example, when enumerable content changes on a `Ember.Set` attribute, it writes back automatically instead of forcing you to call `.set` manually. This functionality can be used by setting an array named observesChildren on your transform containing the names of keys to observe. When one of them triggers it will call childDidChange on your transform with the same arguments as to and from. @param {Object} obj The transformed value that is being observed @param {String} key The key used to access this attribute on the record @param {Object} prev Previous value (not used) @param {Object} context Hash of extra context information */ _EmberRA_childObserver: function(obj, key, prev, context) { // write the new value back to the record this.call(context.record, context.key, obj); // mark the attribute as dirty context.record.notifyPropertyChange(context.key); }, /** Converts the passed value from the core attribute value. This will apply any format transforms. You can install standard transforms by adding to the `Ember.RecordAttribute.transforms` hash. See `Ember.RecordAttribute.registerTransform()` for more. @param {Ember.Record} record The record instance @param {String} key The key used to access this attribute on the record @param {Object} value The transformed value @returns {Object} The value converted back to attribute format */ fromType: function(record, key, value) { var transform = get(this, 'transform'), type = get(this, 'typeClass'); if (transform && transform.from) { value = transform.from(value, this, type, record, key); } return value; }, /** The core handler. Called when `get()` is called on the parent record, since `Ember.RecordAttribute` uses `isProperty` to masquerade as a computed property. Get expects a property be a function, thus we need to implement call. @param {Ember.Record} record The record instance @param {String} key The key used to access this attribute on the record @param {Object} value The property value if called as a setter @returns {Object} property value */ call: function(record, key, value) { var attrKey = get(this, 'key') || key, nvalue; if ((value !== undefined) && get(this, 'isEditable')) { // careful: don't overwrite value here. we want the return value to // cache. nvalue = this.fromType(record, key, value) ; // convert to attribute. record.writeAttribute(attrKey, nvalue); } nvalue = value = record.readAttribute(attrKey); if (Ember.none(value) && (value = get(this, 'defaultValue'))) { if (typeof value === 'function') { value = this.defaultValue(record, key, this); // write default value so it doesn't have to be executed again if ((nvalue !== value) && get(record, 'store').readDataHash(get(record, 'storeKey'))) { record.writeAttribute(attrKey, value, true); } } } else value = this.toType(record, key, value); return value ; }, // .......................................................... // INTERNAL SUPPORT // /** @private - Make this look like a property so that `get()` will call it. */ isProperty: YES, /** @private - Make this look cacheable */ isCacheable: YES, /** @private - needed for KVO `property()` support */ dependentKeys: [], /** @private */ init: function() { this._super(); // setup some internal properties needed for KVO - faking 'cacheable' this.cacheKey = "__cache__" + Ember.guidFor(this) ; this.lastSetValueKey = "__lastValue__" + Ember.guidFor(this) ; }, /** @private Returns a computed property value that can be assigned directly to a property on a record for this attribute. */ computed: function() { var attr = this; var ret = Ember.computed(function(key, value) { return attr.call(this, key, value); }); ret.attr = attr; return ret ; } }) ; // .......................................................... // CLASS METHODS // Ember.RecordAttribute.reopenClass( /** @scope Ember.RecordAttribute.prototype */{ /** The default method used to create a record attribute instance. Unlike `create()`, takes an `attributeType` as the first parameter which will be set on the attribute itself. You can pass a string naming a class or a class itself. @static @param {Object|String} attributeType the assumed attribute type @param {Hash} opts optional additional config options @returns {Ember.RecordAttribute} new instance */ attr: function(attributeType, opts) { if (!opts) opts = {} ; if (!opts.type) opts.type = attributeType || String ; return this.create(opts); }, /** @private Hash of registered transforms by class guid. */ transforms: {}, /** Call to register a transform handler for a specific type of object. The object you pass can be of any type as long as it responds to the following methods - `to(value, attr, klass, record, key)` converts the passed value (which will be of the class expected by the attribute) into the underlying attribute value - `from(value, attr, klass, record, key)` converts the underyling attribute value into a value of the class You can also provide an array of keys to observer on the return value. When any of these change, your from method will be called to write the changed object back to the record. For example: { to: function(value, attr, type, record, key) { if(value) return value.toSet(); else return Ember.Set.create(); }, from: function(value, attr, type, record, key) { return value.toArray(); }, observesChildren: ['[]'] } @static @param {Object} klass the type of object you convert @param {Object} transform the transform object @returns {Ember.RecordAttribute} receiver */ registerTransform: function(klass, transform) { Ember.RecordAttribute.transforms[Ember.guidFor(klass)] = transform; }, /** Retrieves the original record attribute for the passed key. You can't use get() to retrieve record attributes because that will invoke the property instead. @param {Ember.Record} rec record instance to inspect @param {String} keyName key name to retrieve @returns {Ember.RecordAttribute} the attribute or null if none defined */ attrFor: function(rec, keyName) { var ret = Ember.meta(rec, false).descs[keyName]; return ret && ret.attr; } }); // .......................................................... // STANDARD ATTRIBUTE TRANSFORMS // // Object, String, Number just pass through. /** @private - generic converter for Boolean records */ Ember.RecordAttribute.registerTransform(Boolean, { /** @private - convert an arbitrary object value to a boolean */ to: function(obj) { return Ember.none(obj) ? null : !!obj; } }); /** @private - generic converter for Numbers */ Ember.RecordAttribute.registerTransform(Number, { /** @private - convert an arbitrary object value to a Number */ to: function(obj) { return Ember.none(obj) ? null : Number(obj) ; } }); /** @private - generic converter for Strings */ Ember.RecordAttribute.registerTransform(String, { /** @private - convert an arbitrary object value to a String allow null through as that will be checked separately */ to: function(obj) { if (!(typeof obj === 'string') && !Ember.none(obj) && obj.toString) { obj = obj.toString(); } return obj; } }); /** @private - generic converter for Array */ Ember.RecordAttribute.registerTransform(Array, { /** @private - check if obj is an array */ to: function(obj) { if (!Ember.isArray(obj) && !Ember.none(obj)) { obj = []; } return obj; }, observesChildren: ['[]'] }); /** @private - generic converter for Object */ Ember.RecordAttribute.registerTransform(Object, { /** @private - check if obj is an object */ to: function(obj) { if (!(typeof obj === 'object') && !Ember.none(obj)) { obj = {}; } return obj; } }); /** @private - generic converter for Ember.Record-type records */ Ember.RecordAttribute.registerTransform(Ember.Record, { /** @private - convert a record id to a record instance */ to: function(id, attr, recordType, parentRecord) { var store = get(parentRecord, 'store'); if (Ember.none(id) || (id==="")) return null; else return store.find(recordType, id); }, /** @private - convert a record instance to a record id */ from: function(record) { return record ? get(record, 'id') : null; } }); /** @private - generic converter for transforming computed record attributes */ Ember.RecordAttribute.registerTransform('function', { /** @private - convert a record id to a record instance */ to: function(id, attr, recordType, parentRecord) { recordType = recordType.apply(parentRecord); var store = get(parentRecord, 'store'); return store.find(recordType, id); }, /** @private - convert a record instance to a record id */ from: function(record) { return get(record, 'id'); } }); /** @private - generic converter for Date records */ Ember.RecordAttribute.registerTransform(Date, { /** @private - convert a string to a Date */ to: function(str, attr) { // If a null or undefined value is passed, don't // do any normalization. if (Ember.none(str)) { return str; } var ret ; str = str.toString() || ''; if (get(attr, 'useIsoDate')) { var regexp = "([0-9]{4})(-([0-9]{2})(-([0-9]{2})" + "(T([0-9]{2}):([0-9]{2})(:([0-9]{2})(\\.([0-9]+))?)?" + "(Z|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?", d = str.match(new RegExp(regexp)), offset = 0, date = new Date(d[1], 0, 1), time ; if (d[3]) { date.setMonth(d[3] - 1); } if (d[5]) { date.setDate(d[5]); } if (d[7]) { date.setHours(d[7]); } if (d[8]) { date.setMinutes(d[8]); } if (d[10]) { date.setSeconds(d[10]); } if (d[12]) { date.setMilliseconds(Number("0." + d[12]) * 1000); } if (d[14]) { offset = (Number(d[16]) * 60) + Number(d[17]); offset *= ((d[15] === '-') ? 1 : -1); } offset -= date.getTimezoneOffset(); time = (Number(date) + (offset * 60 * 1000)); ret = new Date(); ret.setTime(Number(time)); } else ret = new Date(Date.parse(str)); return ret ; }, _dates: {}, /** @private - pad with leading zeroes */ _zeropad: function(num) { return ((num<0) ? '-' : '') + ((num<10) ? '0' : '') + Math.abs(num); }, /** @private - convert a date to a string */ from: function(date) { if (Ember.none(date)) { return null; } var ret = this._dates[date.getTime()]; if (ret) return ret ; // figure timezone var zp = this._zeropad, tz = 0-date.getTimezoneOffset()/60; tz = (tz === 0) ? 'Z' : '%@:00'.fmt(zp(tz)); this._dates[date.getTime()] = ret = "%@-%@-%@T%@:%@:%@%@".fmt( zp(date.getFullYear()), zp(date.getMonth()+1), zp(date.getDate()), zp(date.getHours()), zp(date.getMinutes()), zp(date.getSeconds()), tz) ; return ret ; } }); if (Ember.DateTime && !Ember.RecordAttribute.transforms[Ember.guidFor(Ember.DateTime)]) { /** Registers a transform to allow `Ember.DateTime` to be used as a record attribute, ie `Ember.Record.attr(Ember.DateTime);` Because `Ember.RecordAttribute` is in the datastore framework and `Ember.DateTime` in the foundation framework, and we don't know which framework is being loaded first, this chunck of code is duplicated in both frameworks. IF YOU EDIT THIS CODE MAKE SURE YOU COPY YOUR CHANGES to `record_attribute.js.` */ Ember.RecordAttribute.registerTransform(Ember.DateTime, { /** @private Convert a String to a DateTime */ to: function(str, attr) { if (Ember.none(str) || (str instanceof Ember.DateTime)) return str; if (Ember.none(str) || (str instanceof Date)) return Ember.DateTime.create(str.getTime()); var format = get(attr, 'format'); return Ember.DateTime.parse(str, format ? format : Ember.DateTime.recordFormat); }, /** @private Convert a DateTime to a String */ from: function(dt, attr) { if (Ember.none(dt)) return dt; var format = get(attr, 'format'); return dt.toFormattedString(format ? format : Ember.DateTime.recordFormat); } }); } /** Parses a coreset represented as an array. */ Ember.RecordAttribute.registerTransform(Ember.Set, { to: function(value, attr, type, record, key) { return Ember.Set.create(value); }, from: function(value, attr, type, record, key) { return value.toArray(); }, observesChildren: ['[]'] }); })({}); (function(exports) { // ========================================================================== // Project: SproutCore - JavaScript Application Framework // Copyright: ©2010 Evin Grano // Portions ©2008-2011 Apple Inc. All rights reserved. // License: Licensed under MIT license (see license.js) // ========================================================================== var get = Ember.get, set = Ember.set; /** @class ChildAttribute is a subclass of `RecordAttribute` and handles to-one relationships for child records. When setting ( `set()` ) the value of a toMany attribute, make sure to pass in an array of `Ember.Record` objects. There are many ways you can configure a ManyAttribute: contacts: Ember.ChildAttribute.attr('Ember.Child'); @extends Ember.RecordAttribute @since SproutCore 1.0 */ Ember.ChildAttribute = Ember.RecordAttribute.extend( /** @scope Ember.ChildAttribute.prototype */ { isNestedRecordTransform: YES, // .......................................................... // LOW-LEVEL METHODS // /** @private - adapted for to one relationship */ toType: function(record, key, value) { var ret = null, rel, recordType = get(this, 'typeClass'); if (!record) { throw 'Ember.Child: Error during transform: Unable to retrieve parent record.'; } if (!Ember.none(value)) ret = record.registerNestedRecord(value, key); return ret; }, // Default fromType is just returning itself fromType: function(record, key, value) { var sk, store, ret; if (record) { if (Ember.none(value)) { // Handle null value. record.writeAttribute(key, value); ret = value; } else { // Register the nested record with this record (the parent). ret = record.registerNestedRecord(value, key); if (ret) { // Write the data hash of the nested record to the store. sk = get(ret, 'storeKey'); store = get(ret, 'store'); record.writeAttribute(key, store.readDataHash(sk)); } else if (value) { // If registration failed, just write the value. record.writeAttribute(key, value); } } } return ret; }, /** The core handler. Called from the property. @param {Ember.Record} record the record instance @param {String} key the key used to access this attribute on the record @param {Object} value the property value if called as a setter @returns {Object} property value */ call: function(record, key, value) { var attrKey = get(this, 'key') || key, cRef, cacheKey = '__kid__'+Ember.guidFor(this); if (value !== undefined) { // this.orphan(record, cacheKey, value); value = this.fromType(record, key, value) ; // convert to attribute. // record[cacheKey] = value; } else { value = record.readAttribute(attrKey); if (Ember.none(value) && (value = get(this, 'defaultValue'))) { if (typeof value === 'function') { value = this.defaultValue(record, key, this); // write default value so it doesn't have to be executed again if(get(record, 'attributes')) record.writeAttribute(attrKey, value, true); } } else value = this.toType(record, key, value); } return value ; } }); })({}); (function(exports) { // ========================================================================== // Project: SproutCore - JavaScript Application Framework // Copyright: ©2010 Evin Grano // Portions ©2008-2011 Apple Inc. All rights reserved. // License: Licensed under MIT license (see license.js) // ========================================================================== var get = Ember.get, set = Ember.set, getPath = Ember.getPath; /** @class A `ChildArray` is used to map an array of `ChildRecord` objects. @extends Ember.Enumerable @extends Ember.Array @since SproutCore 1.0 */ Ember.ChildArray = Ember.Object.extend(Ember.Enumerable, Ember.Array, Ember.MutableEnumerable, Ember.MutableArray, /** @scope Ember.ChildArray.prototype */ { /** If set, it is the default record `recordType` @default null @type String */ defaultRecordType: null, /** If set, the parent record will be notified whenever the array changes so that it can change its own state @default null @type {Ember.Record} */ record: null, /** If set will be used by the many array to get an editable version of the `storeId`s from the owner. @default null @type String */ propertyName: null, /** Actual references to the hashes @default null @type {Ember.Array} */ children: null, /** The store that owns this record array. All record arrays must have a store to function properly. @type Ember.Store @property */ store: function() { return getPath(this, 'record.store'); }.property('record').cacheable(), /** The storeKey for the parent record of this many array. Editing this array will place the parent record into a `READY_DIRTY state. @type Number @property */ storeKey: function() { return getPath(this, 'record.storeKey'); }.property('record').cacheable(), /** Returns the storeIds in read only mode. Avoids modifying the record unnecessarily. @type Ember.Array @property */ readOnlyChildren: function() { return get(this, 'record').readAttribute(get(this, 'propertyName')); }.property(), /** Returns an editable array of child hashes. Marks the owner records as modified. @type {Ember.Array} @property */ editableChildren: function() { var store = get(this, 'store'), storeKey = get(this, 'storeKey'), pname = get(this, 'propertyName'), ret, hash; ret = store.readEditableProperty(storeKey, pname); if (!ret) { hash = store.readEditableDataHash(storeKey); ret = hash[pname] = []; } if (ret !== this._prevChildren) this.recordPropertyDidChange(); return ret ; }.property(), // .......................................................... // ARRAY PRIMITIVES // /** @private Returned length is a pass-through to the storeIds array. @type Number @property */ length: function() { var children = get(this, 'readOnlyChildren'); return children ? children.length : 0; }.property('readOnlyChildren'), /** Looks up the store id in the store ids array and materializes a records. @param {Number} idx index of the object to retrieve. @returns {Ember.Record} The record if found or undefined. */ objectAt: function(idx) { var recs = this._records, children = get(this, 'readOnlyChildren'), hash, ret, pname = get(this, 'propertyName'), parent = get(this, 'record'); var len = children ? children.length : 0; if (!children) return undefined; // nothing to do if (recs && (ret=recs[idx])) return ret ; // cached if (!recs) this._records = recs = [] ; // create cache // If not a good index return undefined if (idx >= len) return undefined; hash = children.objectAt(idx); if (!hash) return undefined; // not in cache, materialize recs[idx] = ret = parent.registerNestedRecord(hash, pname, pname+'.'+idx); return ret; }, /** Pass through to the underlying array. The passed in objects must be records, which can be converted to `storeId`s. @param {Number} idx index of the object to replace. @param {Number} amt number of records to replace starting at idx. @param {Number} recs array with records to replace. @returns {Ember.Record} The record if found or undefined. */ replace: function(idx, amt, recs) { var children = get(this, 'editableChildren'), len = recs ? get(recs, 'length') : 0, record = get(this, 'record'), newRecs, pname = get(this, 'propertyName'), cr, recordType; newRecs = this._processRecordsToHashes(recs); children.replace(idx, amt, newRecs); // notify that the record did change... record.recordDidChange(pname); return this; }, /** @private Converts a records array into an array of hashes. @param {Ember.Array} recs records to be converted to hashes. @returns {Ember.Array} array of hashes. */ _processRecordsToHashes: function(recs){ var store, sk; recs = recs || []; recs.forEach( function(me, idx) { if (me instanceof Ember.Record) { store = get(me, 'store'); sk = get(me, 'storeKey'); if (sk) recs[idx] = store.readDataHash(sk); } }); return recs; }, /** Calls normalize on each object in the array */ normalize: function(){ this.forEach(function(child,id){ if(child.normalize) child.normalize(); }); }, // .......................................................... // INTERNAL SUPPORT // /** Invoked whenever the children array changes. Observes changes. @param {Ember.Array} keys optional @returns {Ember.ChildArray} itself. */ recordPropertyDidChange: function(keys) { if (keys && !keys.contains(get(this, 'propertyName'))) return this; var children = get(this, 'readOnlyChildren'), oldLen = 0, newLen = 0; var prev = this._prevChildren, f = this._childrenContentDidChange; if (children === prev) return this; // nothing to do if (prev) { prev.removeArrayObserver(this, { willChange: this.arrayContentWillChange, didChange: f }); oldLen = get(prev, 'length'); } if (children) { children.addArrayObserver(this, { willChange: this.arrayContentWillChange, didChange: f }); newLen = get(children, 'length'); } this.arrayContentWillChange(0, oldLen, newLen); this._prevChildren = children; this._childrenContentDidChange(children, 0, oldLen, newLen); return this; }, /** @private Invoked whenever the content of the children array changes. This will dump any cached record lookup and then notify that the enumerable content has changed. @param {Number} target @param {Number} key @param {Number} value @param {Number} rev */ _childrenContentDidChange: function(content, start, removedCount, addedCount) { this._records = null ; // clear cache this.arrayContentDidChange(start, removedCount, addedCount); }, /** @private */ init: function() { this._super(); this.recordPropertyDidChange(); } }) ; })({}); (function(exports) { // ========================================================================== // Project: SproutCore - JavaScript Application Framework // Copyright: ©2010 Evin Grano // Portions ©2008-2011 Apple Inc. All rights reserved. // License: Licensed under MIT license (see license.js) // ========================================================================== var get = Ember.get, set = Ember.set; /** @class ChildrenAttribute is a subclass of ChildAttribute and handles to-many relationships for child records. When setting ( `set()` ) the value of a toMany attribute, make sure to pass in an array of Ember.Record objects. There are many ways you can configure a ChildrenAttribute: contacts: Ember.ChildrenAttribute.attr('Ember.Child'); @extends Ember.RecordAttribute @since SproutCore 1.0 */ Ember.ChildrenAttribute = Ember.ChildAttribute.extend( /** @scope Ember.ChildrenAttribute.prototype */ { // .......................................................... // LOW-LEVEL METHODS // /** @private - adapted for to many relationship */ toType: function(record, key, value) { var attrKey = get(this, 'key') || key, arrayKey = '__kidsArray__'+Ember.guidFor(this), ret = record[arrayKey], recordType = get(this, 'typeClass'), rel; // lazily create a ManyArray one time. after that always return the // same object. if (!ret) { ret = Ember.ChildArray.create({ record: record, propertyName: attrKey, defaultRecordType: recordType }); record[arrayKey] = ret ; // save on record rel = get(record, 'relationships'); if (!rel) set(record, 'relationships', rel = []); rel.push(ret); // make sure we get notified of changes... } return ret; }, // Default fromType is just returning itself fromType: function(record, key, value){ var sk, store, arrayKey = '__kidsArray__'+Ember.guidFor(this), ret = record[arrayKey]; if (record) { record.writeAttribute(key, value); if (ret) ret = ret.recordPropertyDidChange(); } return ret; } }); })({}); (function(exports) { // ========================================================================== // 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) // ========================================================================== var get = Ember.get, set = Ember.set, attrFor = Ember.RecordAttribute.attrFor; /** @class A `ManyArray` is used to map an array of record ids back to their record objects which will be materialized from the owner store on demand. Whenever you create a `toMany()` relationship, the value returned from the property will be an instance of `ManyArray`. You can generally customize the behavior of ManyArray by passing settings to the `toMany()` helper. @extends Ember.Enumerable @extends Ember.Array @since SproutCore 1.0 */ Ember.ManyArray = Ember.Object.extend(Ember.Enumerable, Ember.MutableEnumerable, Ember.MutableArray, Ember.Array, /** @scope Ember.ManyArray.prototype */ { /** `recordType` will tell what type to transform the record to when materializing the record. @default null @type String */ recordType: null, /** If set, the record will be notified whenever the array changes so that it can change its own state @default null @type Ember.Record */ record: null, /** If set will be used by the many array to get an editable version of the storeIds from the owner. @default null @type String */ propertyName: null, /** The `ManyAttribute` that created this array. @default null @type Ember.ManyAttribute */ manyAttribute: null, /** The store that owns this record array. All record arrays must have a store to function properly. @type Ember.Store @property */ store: function() { return get(get(this, 'record'), 'store'); }.property('record').cacheable(), /** The `storeKey` for the parent record of this many array. Editing this array will place the parent record into a `READY_DIRTY` state. @type Number @property */ storeKey: function() { return get(get(this, 'record'), 'storeKey'); }.property('record').cacheable(), /** Returns the `storeId`s in read-only mode. Avoids modifying the record unnecessarily. @type Ember.Array @property */ readOnlyStoreIds: function() { return get(this, 'record').readAttribute(get(this, 'propertyName')); }.property(), /** Returns an editable array of `storeId`s. Marks the owner records as modified. @type {Ember.Array} @property */ editableStoreIds: function() { var store = get(this, 'store'), storeKey = get(this, 'storeKey'), pname = get(this, 'propertyName'), ret, hash; ret = store.readEditableProperty(storeKey, pname); if (!ret) { hash = store.readEditableDataHash(storeKey); ret = hash[pname] = []; } if (ret !== this._prevStoreIds) this.recordPropertyDidChange(); return ret ; }.property(), // .......................................................... // COMPUTED FROM OWNER // /** Computed from owner many attribute @type Boolean @property */ isEditable: function() { // NOTE: can't use get() b/c manyAttribute looks like a computed prop var attr = this.manyAttribute; return attr ? get(attr, 'isEditable') : NO; }.property('manyAttribute').cacheable(), /** Computed from owner many attribute @type String @property */ inverse: function() { // NOTE: can't use get() b/c manyAttribute looks like a computed prop var attr = this.manyAttribute; return attr ? get(attr, 'inverse') : null; }.property('manyAttribute').cacheable(), /** Computed from owner many attribute @type Boolean @property */ isMaster: function() { // NOTE: can't use get() b/c manyAttribute looks like a computed prop var attr = this.manyAttribute; return attr ? get(attr, 'isMaster') : null; }.property("manyAttribute").cacheable(), /** Computed from owner many attribute @type Array @property */ orderBy: function() { // NOTE: can't use get() b/c manyAttribute looks like a computed prop var attr = this.manyAttribute; return attr ? get(attr, 'orderBy') : null; }.property("manyAttribute").cacheable(), // .......................................................... // ARRAY PRIMITIVES // /** @private Returned length is a pass-through to the `storeIds` array. @type Number @property */ length: function() { var storeIds = get(this, 'readOnlyStoreIds'); return storeIds ? get(storeIds, 'length') : 0; }.property('readOnlyStoreIds'), /** @private Looks up the store id in the store ids array and materializes a records. */ objectAt: function(idx) { var recs = this._records, storeIds = get(this, 'readOnlyStoreIds'), store = get(this, 'store'), recordType = get(this, 'recordType'), storeKey, ret, storeId ; if (!storeIds || !store) return undefined; // nothing to do if (recs && (ret=recs[idx])) return ret ; // cached // not in cache, materialize if (!recs) this._records = recs = [] ; // create cache storeId = storeIds.objectAt(idx); if (storeId) { // if record is not loaded already, then ask the data source to // retrieve it storeKey = store.storeKeyFor(recordType, storeId); if (store.readStatus(storeKey) === Ember.Record.EMPTY) { store.retrieveRecord(recordType, null, storeKey); } recs[idx] = ret = store.materializeRecord(storeKey); } return ret ; }, /** @private Pass through to the underlying array. The passed in objects must be records, which can be converted to `storeId`s. */ replace: function(idx, amt, recs) { if (!get(this, 'isEditable')) { throw "%@.%@[] is not editable".fmt(get(this, 'record'), get(this, 'propertyName')); } var storeIds = get(this, 'editableStoreIds'), len = recs ? get(recs, 'length') : 0, record = get(this, 'record'), pname = get(this, 'propertyName'), i, keys, ids, toRemove, inverse, attr, inverseRecord; // map to store keys ids = [] ; for(i=0;i0) { toRemove = Ember.ManyArray._toRemove; if (toRemove) Ember.ManyArray._toRemove = null; // reuse if possible else toRemove = []; for(i=0;i= 0) { storeIds.removeAt(idx); if (get(this, 'isMaster') && (record = get(this, 'record'))) { record.recordDidChange(get(this, 'propertyName')); } } return this; }, _inverseRecordDidLoad: function(obj, key, val) { var store = get(this, 'store'); var id = store.idFor(obj.get("storeKey")); if(id) { obj.removeObserver("status", this, "_inverseRecordDidLoad"); this.addInverseRecord(obj); } }, /** Called by the `ManyAttribute` whenever a record is added on the inverse of the relationship. @param {Ember.Record} inverseRecord the record this array is a part of @returns {Ember.ManyArray} receiver */ addInverseRecord: function(inverseRecord) { if (!inverseRecord) return this; var store = get(this, 'store'); var id = store.idFor(inverseRecord.get("storeKey")); if(!id) { inverseRecord.addObserver("status", this, "_inverseRecordDidLoad"); return this; } var storeIds = get(this, 'editableStoreIds'), orderBy = get(this, 'orderBy'), len = get(storeIds, 'length'), idx, record; // find idx to insert at. if (orderBy) { idx = this._findInsertionLocation(inverseRecord, 0, len, orderBy); } else idx = len; storeIds.insertAt(idx, get(inverseRecord, 'id')); if (get(this, 'isMaster') && (record = get(this, 'record'))) { record.recordDidChange(get(this, 'propertyName')); } return this; }, /** @private binary search to find insertion location */ _findInsertionLocation: function(rec, min, max, orderBy) { var idx = min+Math.floor((max-min)/2), cur = this.objectAt(idx), order = this._compare(rec, cur, orderBy); if (order < 0) { if (idx===0) return idx; else return this._findInsertionLocation(rec, 0, idx, orderBy); } else if (order > 0) { if (idx >= max) return idx; else return this._findInsertionLocation(rec, idx, max, orderBy); } else return idx; }, /** @private function to compare to objects */ _compare: function(a, b, orderBy) { var t = Ember.typeOf(orderBy), ret, idx, len; if (t === 'function') ret = orderBy(a, b); else if (t === 'string') ret = Ember.compare(a,b); else { len = get(orderBy, 'length'); ret = 0; for(idx=0;(ret===0) && (idx storeKey, isEditable: YES|NO, make editable or not, through: 'taggings' // set a relationship this goes through }); @extends Ember.RecordAttribute @since SproutCore 1.0 */ Ember.ManyAttribute = Ember.RecordAttribute.extend( /** @scope Ember.ManyAttribute.prototype */ { /** Set the foreign key on content objects that represent the inversion of this relationship. The inverse property should be a `toOne()` or `toMany()` relationship as well. Modifying this many array will modify the `inverse` property as well. @property {String} */ inverse: null, /** If `YES` then modifying this relationships will mark the owner record dirty. If set to `NO`, then modifying this relationship will not alter this record. You should use this property only if you have an inverse property also set. Only one of the inverse relationships should be marked as master so you can control which record should be committed. @property {Boolean} */ isMaster: YES, /** If set and you have an inverse relationship, will be used to determine the order of an object when it is added to an array. You can pass a function or an array of property keys. @property {Function|Array} */ orderBy: null, // .......................................................... // LOW-LEVEL METHODS // /** @private - adapted for to many relationship */ toType: function(record, key, value) { var type = get(this, 'typeClass'), attrKey = get(this, 'key') || key, arrayKey = '__manyArray__'+Ember.guidFor(this), ret = record[arrayKey], rel; // lazily create a ManyArray one time. after that always return the // same object. if (!ret) { ret = Ember.ManyArray.create({ recordType: type, record: record, propertyName: attrKey, manyAttribute: this }); record[arrayKey] = ret ; // save on record rel = get(record, 'relationships'); if (!rel) set(record, 'relationships', rel = []); rel.push(ret); // make sure we get notified of changes... } return ret; }, /** @private - adapted for to many relationship */ fromType: function(record, key, value) { var ret = []; if(!Ember.isArray(value)) throw "Expects toMany attribute to be an array"; var len = get(value, 'length'); for(var i=0;i storeKey, isEditable: YES|NO, make editable or not }); @extends Ember.RecordAttribute @since SproutCore 1.0 */ Ember.SingleAttribute = Ember.RecordAttribute.extend( /** @scope Ember.SingleAttribute.prototype */ { /** Specifies the property on the member record that represents the inverse of the current relationship. If set, then modifying this relationship will also alter the opposite side of the relationship. @type String @default null */ inverse: null, /** If set, determines that when an inverse relationship changes whether this record should become dirty also or not. @type Boolean @default YES */ isMaster: YES, /** @private - implements support for handling inverse relationships. */ call: function(record, key, newRec) { var attrKey = get(this, 'key') || key, inverseKey, isMaster, oldRec, attr, ret, nvalue; // WRITE if (newRec !== undefined && get(this, 'isEditable')) { // can only take other records or null if (newRec && !(newRec instanceof Ember.Record)) { throw "%@ is not an instance of Ember.Record".fmt(newRec); } inverseKey = get(this, 'inverse'); if (inverseKey) oldRec = this._super(record, key); // careful: don't overwrite value here. we want the return value to // cache. nvalue = this.fromType(record, key, newRec) ; // convert to attribute. record.writeAttribute(attrKey, nvalue, !get(this, 'isMaster')); ret = newRec ; // ok, now if we have an inverse relationship, get the inverse // relationship and notify it of what is happening. This will allow it // to update itself as needed. The callbacks implemented here are // supported by both SingleAttribute and ManyAttribute. // if (inverseKey && (oldRec !== newRec)) { if (oldRec && (attr = attrFor(oldRec, inverseKey))) { attr.inverseDidRemoveRecord(oldRec, inverseKey, record, key); } if (newRec && (attr = attrFor(newRec, inverseKey))) { attr.inverseDidAddRecord(newRec, inverseKey, record, key); } } // READ } else ret = this._super(record, key, newRec); return ret ; }, /** Called by an inverse relationship whenever the receiver is no longer part of the relationship. If this matches the inverse setting of the attribute then it will update itself accordingly. @param {Ember.Record} record the record owning this attribute @param {String} key the key for this attribute @param {Ember.Record} inverseRecord record that was removed from inverse @param {String} inverseKey key on inverse that was modified */ inverseDidRemoveRecord: function(record, key, inverseRecord, inverseKey) { var myInverseKey = get(this, 'inverse'), curRec = RecordAttribute_call.call(this, record, key), isMaster = get(this, 'isMaster'), attr; // ok, you removed me, I'll remove you... if isMaster, notify change. record.writeAttribute(key, null, !isMaster); record.notifyPropertyChange(key); // if we have another value, notify them as well... if ((curRec !== inverseRecord) || (inverseKey !== myInverseKey)) { if (curRec && (attr = attrFor(curRec, myInverseKey))) { attr.inverseDidRemoveRecord(curRec, myInverseKey, record, key); } } }, /** Called by an inverse relationship whenever the receiver is added to the inverse relationship. This will set the value of this inverse record to the new record. @param {Ember.Record} record the record owning this attribute @param {String} key the key for this attribute @param {Ember.Record} inverseRecord record that was added to inverse @param {String} inverseKey key on inverse that was modified */ inverseDidAddRecord: function(record, key, inverseRecord, inverseKey) { var myInverseKey = get(this, 'inverse'), curRec = RecordAttribute_call.call(this, record, key), isMaster = get(this, 'isMaster'), attr, nvalue; // ok, replace myself with the new value... nvalue = this.fromType(record, key, inverseRecord); // convert to attr. record.writeAttribute(key, nvalue, !isMaster); record.notifyPropertyChange(key); // if we have another value, notify them as well... if ((curRec !== inverseRecord) || (inverseKey !== myInverseKey)) { if (curRec && (attr = attrFor(curRec, myInverseKey))) { attr.inverseDidRemoveRecord(curRec, myInverseKey, record, key); } } } }); })({}); (function(exports) { // ========================================================================== // Project: SproutCore DataStore // Copyright: ©2006-2011 Strobe Inc. and contributors. // Portions ©2008-2011 Apple Inc. All rights reserved. // License: Licensed under MIT license (see license.js) // ========================================================================== })({}); (function(exports) { // ========================================================================== // 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) // ========================================================================== /** Indicates a value has a mixed state of both on and off. @property {String} */ Ember.MIXED_STATE = '__MIXED__'; /** @class A DataSource connects an in-memory store to one or more server backends. To connect to a data backend on a server, subclass `Ember.DataSource` and implement the necessary data source methods to communicate with the particular backend. ## Create a Data Source To implement the data source, subclass `Ember.DataSource` in a file located either in the root level of your app or framework, or in a directory called "data_sources": MyApp.DataSource = Ember.DataSource.extend({ // implement the data source API... }); ## Connect to a Data Source New SproutCore applications are wired up to fixtures as their data source. When you are ready to connect to a server, swap the use of fixtures with a call to the desired data source. In core.js: // change... store: Ember.Store.create().from(Ember.Record.fixtures) // to... store: Ember.Store.create().from('MyApp.DataSource') Note that the data source class name is referenced by string since the file in which it is defined may not have been loaded yet. The first time a data store tries to access its data source it will look up the class name and instantiate that data source. ## Implement the Data Source API There are three methods that a data store invokes on its data source: * `fetch()` — called the first time you try to `find()` a query on a store or any time you refresh the record array after that. * `retrieveRecords()` — called when you access an individual record that has not been loaded yet * `commitRecords()` — called if the the store has changes pending and its `commitRecords()` method is invoked. The data store will call the `commitRecords()` method when records need to be created, updated, or deleted. If the server that the data source connects to handles these three actions in a uniform manner, it may be convenient to implement the `commitRecords()` to handle record creation, updating, and deletion. However, if the calls the data source will need to make to the server to create, update, and delete records differ from each other to a significant enough degree, it will be more convenient to rely on the default behavior of `commitRecords()` and instead implement the three methods that it will call by default: * `createRecords()` — called with a list of records that are new and need to be created on the server. * `updateRecords()` — called with a list of records that already exist on the server but that need to be updated. * `destroyRecords()` — called with a list of records that should be deleted on the server. ### Multiple records The `retrieveRecords()`, `createRecords()`, `updateRecords()` and `destroyRecords()` methods all work on multiple records. If your server API accommodates calls where you can pass a list of records, this might be the best level at which to implement the Data Source API. On the other hand, if the server requires that you send commands for it for individual records, you can rely on the default implementation of these four methods, which will call the following for each individual record, one at a time: - `retrieveRecord()` — called to retrieve a single record. - `createRecord()` — called to create a single record. - `updateRecord()` — called to update a single record. - `destroyRecord()` — called to destroy a single record. ### Return Values All of the methods you implement must return one of three values: - `YES` — all the records were handled. - `NO` — none of the records were handled. - `Ember.MIXED_STATE` — some, but not all of the records were handled. ### Store Keys Whenever a data store invokes one of the data source methods it does so with a storeKeys or storeKey argument. Store keys are transient integers assigned to each data hash when it is first loaded into the store. It is used to track data hashes as they move up and down nested stores (even if no associated record is ever created from it). When passed a storeKey you can use it to retrieve the status, data hash, record type, or record ID, using the following data store methods: * `readDataHash(storeKey)` — returns the data hash associated with a store key, if any. * `readStatus(storeKey)` — returns the current record status associated with the store key. May be `Ember.Record.EMPTY`. * `Ember.Store.recordTypeFor(storeKey)` — returns the record type for the associated store key. * `recordType.idFor(storeKey)` — returns the record ID for the associated store key. You must call this method on `Ember.Record` subclass itself, not on an instance of `Ember.Record`. These methods are safe for reading data from the store. To modify data in the data store you must use the store callbacks described below. The store callbacks will ensure that the record states remain consistent. ### Store Callbacks When a data store calls a data source method, it puts affected records into a `BUSY` state. To guarantee data integrity and consistency, these records cannot be modified by the rest of the application while they are in the `BUSY` state. Because records are "locked" while in the `BUSY` state, it is the data source's responsibility to invoke a callback on the store for each record or query that was passed to it and that the data source handled. To reduce the amount of work that a data source must do, the data store will automatically unlock the relevant records if the the data source method returned `NO`, indicating that the records were unhandled. Although a data source can invoke callback methods at any time, they should usually be invoked after receiving a response from the server. For example, when the data source commits a change to a record by issuing a command to the server, it waits for the server to acknowledge the command before invoking the `dataSourceDidComplete()` callback. In some cases a data source may be able to assume a server's response and invoke the callback on the store immediately. This can improve performance because the record can be unlocked right away. ### Record-Related Callbacks When `retrieveRecords()`, `commitRecords()`, or any of the related methods are called on a data source, the store puts any records to be handled by the data store in a `BUSY` state. To release the records the data source must invoke one of the record-related callbacks on the store: * `dataSourceDidComplete(storeKey, dataHash, id)` — the most common callback. You might use this callback when you have retrieved a record to load its contents into the store. The callback tells the store that the data source is finished with the storeKey in question. The `dataHash` and `id` arguments are optional and will replace the current dataHash and/or id. Also see "Loading Records" below. * `dataSourceDidError(storeKey, error)` — a data source should call this when a request could not be completed because an error occurred. The error argument is optional and can contain more information about the error. * `dataSourceDidCancel(storeKey)` — a data source should call this when an operation is cancelled for some reason. This could be used when the user is able to cancel an operation that is in progress. ### Loading Records into the Store Instead of orchestrating multiple `dataSourceDidComplete()` callbacks when loading multiple records, a data source can call the `loadRecords()` method on the store, passing in a `recordType`, and array of data hashes, and optionally an array of ids. The `loadRecords()` method takes care of looking up storeKeys and calling the `dataSourceDidComplete()` callback as needed. `loadRecords()` is often the most convenient way to get large blocks of data into the store, especially in response to a `fetch()` or `retrieveRecords()` call. ### Query-Related Callbacks Like records, queries that are passed through the `fetch()` method also have an associated status property; accessed through the `status` property on the record array returned from `find()`. To properly reset this status, a data source must invoke an appropriate query-related callback on the store. The callbacks for queries are similar to those for records: * `dataSourceDidFetchQuery(query)` — the data source must call this when it has completed fetching any related data for the query. This returns the query results (record array) status into a `READY` state. * `dataSourceDidErrorQuery(query, error)` — the data source should call this if it encounters an error in executing the query. This puts the query results into an `ERROR` state. * `dataSourceDidCancelQuery(query)` — the data source should call this if loading the results is cancelled. In addition to these callbacks, the method `loadQueryResults(query, storeKey)` is used by data sources when handling remote queries. This method is similar to `dataSourceDidFetchQuery()`, except that you also provide an array of storeKeys (or a promise to provide store keys) that comprises the result set. @extend Ember.Object @since SproutCore 1.0 */ Ember.DataSource = Ember.Object.extend( /** @scope Ember.DataSource.prototype */ { // .......................................................... // Ember.STORE ENTRY POINTS // /** Invoked by the store whenever it needs to retrieve data matching a specific query, triggered by find(). This method is called anytime you invoke Ember.Store#find() with a query or Ember.RecordArray#refresh(). You should override this method to actually retrieve data from the server needed to fulfill the query. If the query is a remote query, then you will also need to provide the contents of the query as well. ### Handling Local Queries Most queries you create in your application will be local queries. Local queries are populated automatically from whatever data you have in memory. When your fetch() method is called on a local queries, all you need to do is load any records that might be matched by the query into memory. The way you choose which queries to fetch is up to you, though usually it can be something fairly straightforward such as loading all records of a specified type. When you finish loading any data that might be required for your query, you should always call Ember.Store#dataSourceDidFetchQuery() to put the query back into the READY state. You should call this method even if you choose not to load any new data into the store in order to notify that the store that you think it is ready to return results for the query. ### Handling Remote Queries Remote queries are special queries whose results will be populated by the server instead of from memory. Usually you will only need to use this type of query when loading large amounts of data from the server. Like Local queries, to fetch a remote query you will need to load any data you need to fetch from the server and add the records to the store. Once you are finished loading this data, however, you must also call Ember.Store#loadQueryResults() to actually set an array of storeKeys that represent the latest results from the server. This will implicitly also call datasSourceDidFetchQuery() so you don't need to call this method yourself. If you want to support incremental loading from the server for remote queries, you can do so by passing a Ember.SparseArray instance instead of a regular array of storeKeys and then populate the sparse array on demand. ### Handling Errors and Cancelations If you encounter an error while trying to fetch the results for a query you can call Ember.Store#dataSourceDidErrorQuery() instead. This will put the query results into an error state. If you had to cancel fetching a query before the results were returned, you can instead call Ember.Store#dataSourceDidCancelQuery(). This will set the query back into the state it was in previously before it started loading the query. ### Return Values When you return from this method, be sure to return a Boolean. YES means you handled the query, NO means you can't handle the query. When using a cascading data source, returning NO will mean the next data source will be asked to fetch the same results as well. @param {Ember.Store} store the requesting store @param {Ember.Query} query query describing the request @returns {Boolean} YES if you can handle fetching the query, NO otherwise */ fetch: function(store, query) { return NO ; // do not handle anything! }, /** Called by the store whenever it needs to load a specific set of store keys. The default implementation will call retrieveRecord() for each storeKey. You should implement either retrieveRecord() or retrieveRecords() to actually fetch the records referenced by the storeKeys . @param {Ember.Store} store the requesting store @param {Array} storeKeys @param {Array} ids - optional @returns {Boolean} YES if handled, NO otherwise */ retrieveRecords: function(store, storeKeys, ids) { return this._handleEach(store, storeKeys, this.retrieveRecord, ids); }, /** Invoked by the store whenever it has one or more records with pending changes that need to be sent back to the server. The store keys will be separated into three categories: - `createStoreKeys`: records that need to be created on server - `updateStoreKeys`: existing records that have been modified - `destroyStoreKeys`: records need to be destroyed on the server If you do not override this method yourself, this method will actually invoke `createRecords()`, `updateRecords()`, and `destroyRecords()` on the dataSource, passing each array of storeKeys. You can usually implement those methods instead of overriding this method. However, if your server API can sync multiple changes at once, you may prefer to override this method instead. To support cascading data stores, be sure to return `NO` if you cannot handle any of the keys, `YES` if you can handle all of the keys, or `Ember.MIXED_STATE` if you can handle some of them. @param {Ember.Store} store the requesting store @param {Array} createStoreKeys keys to create @param {Array} updateStoreKeys keys to update @param {Array} destroyStoreKeys keys to destroy @param {Hash} params to be passed down to data source. originated from the commitRecords() call on the store @returns {Boolean} YES if data source can handle keys */ commitRecords: function(store, createStoreKeys, updateStoreKeys, destroyStoreKeys, params) { var uret, dret, ret; if (createStoreKeys.length>0) { ret = this.createRecords.call(this, store, createStoreKeys, params); } if (updateStoreKeys.length>0) { uret = this.updateRecords.call(this, store, updateStoreKeys, params); ret = Ember.none(ret) ? uret : (ret === uret) ? ret : Ember.MIXED_STATE; } if (destroyStoreKeys.length>0) { dret = this.destroyRecords.call(this, store, destroyStoreKeys, params); ret = Ember.none(ret) ? dret : (ret === dret) ? ret : Ember.MIXED_STATE; } return ret || NO; }, /** Invoked by the store whenever it needs to cancel one or more records that are currently in-flight. If any of the storeKeys match records you are currently acting upon, you should cancel the in-progress operation and return `YES`. If you implement an in-memory data source that immediately services the other requests, then this method will never be called on your data source. To support cascading data stores, be sure to return `NO` if you cannot retrieve any of the keys, `YES` if you can retrieve all of the, or `Ember.MIXED_STATE` if you can retrieve some of the. @param {Ember.Store} store the requesting store @param {Array} storeKeys array of storeKeys to retrieve @returns {Boolean} YES if data source can handle keys */ cancel: function(store, storeKeys) { return NO; }, // .......................................................... // BULK RECORD ACTIONS // /** Called from `commitRecords()` to commit modified existing records to the store. You can override this method to actually send the updated records to your store. The default version will simply call `updateRecord()` for each storeKey. To support cascading data stores, be sure to return `NO` if you cannot handle any of the keys, `YES` if you can handle all of the keys, or `Ember.MIXED_STATE` if you can handle some of them. @param {Ember.Store} store the requesting store @param {Array} storeKeys keys to update @param {Hash} params to be passed down to data source. originated from the commitRecords() call on the store @returns {Boolean} YES, NO, or Ember.MIXED_STATE */ updateRecords: function(store, storeKeys, params) { return this._handleEach(store, storeKeys, this.updateRecord, null, params); }, /** Called from `commitRecords()` to commit newly created records to the store. You can override this method to actually send the created records to your store. The default version will simply call `createRecord()` for each storeKey. To support cascading data stores, be sure to return `NO` if you cannot handle any of the keys, `YES` if you can handle all of the keys, or `Ember.MIXED_STATE` if you can handle some of them. @param {Ember.Store} store the requesting store @param {Array} storeKeys keys to update @param {Hash} params to be passed down to data source. originated from the commitRecords() call on the store @returns {Boolean} YES, NO, or Ember.MIXED_STATE */ createRecords: function(store, storeKeys, params) { return this._handleEach(store, storeKeys, this.createRecord, null, params); }, /** Called from `commitRecords()` to commit destroted records to the store. You can override this method to actually send the destroyed records to your store. The default version will simply call `destroyRecord()` for each storeKey. To support cascading data stores, be sure to return `NO` if you cannot handle any of the keys, `YES` if you can handle all of the keys, or `Ember.MIXED_STATE` if you can handle some of them. @param {Ember.Store} store the requesting store @param {Array} storeKeys keys to update @param {Hash} params to be passed down to data source. originated from the commitRecords() call on the store @returns {Boolean} YES, NO, or Ember.MIXED_STATE */ destroyRecords: function(store, storeKeys, params) { return this._handleEach(store, storeKeys, this.destroyRecord, null, params); }, /** @private invokes the named action for each store key. returns proper value */ _handleEach: function(store, storeKeys, action, ids, params) { var len = storeKeys.length, idx, ret, cur, idOrParams; for(idx=0;idx=0) { source = sources[idx]; if (Ember.typeOf(source) === 'string') sources[idx] = get(this, source); } }, /** @private - Determine the proper return value. */ _handleResponse: function(current, response) { if (response === YES) return YES ; else if (current === NO) return (response === NO) ? NO : Ember.MIXED_STATE ; else return Ember.MIXED_STATE ; } }); })({}); (function(exports) { // ========================================================================== // 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) // ========================================================================== var get = Ember.get, set = Ember.set, getPath = Ember.getPath; /** @class TODO: Describe Class @extends Ember.DataSource @since SproutCore 1.0 */ Ember.FixturesDataSource = Ember.DataSource.extend( /** @scope Ember.FixturesDataSource.prototype */ { /** If YES then the data source will asynchronously respond to data requests from the server. If you plan to replace the fixture data source with a data source that talks to a real remote server (using Ajax for example), you should leave this property set to YES so that Fixtures source will more accurately simulate your remote data source. If you plan to replace this data source with something that works with local storage, for example, then you should set this property to NO to accurately simulate the behavior of your actual data source. @property {Boolean} */ simulateRemoteResponse: NO, /** If you set simulateRemoteResponse to YES, then the fixtures source will assume a response latency from your server equal to the msec specified here. You should tune this to simulate latency based on the expected performance of your server network. Here are some good guidelines: - 500: Simulates a basic server written in PHP, Ruby, or Python (not twisted) without a CDN in front for caching. - 250: (Default) simulates the average latency needed to go back to your origin server from anywhere in the world. assumes your servers itself will respond to requests < 50 msec - 100: simulates the latency to a "nearby" server (i.e. same part of the world). Suitable for simulating locally hosted servers or servers with multiple data centers around the world. - 50: simulates the latency to an edge cache node when using a CDN. Life is really good if you can afford this kind of setup. @property {Number} */ latency: 50, // .......................................................... // CANCELLING // /** @private */ cancel: function(store, storeKeys) { return NO; }, // .......................................................... // FETCHING // /** @private */ fetch: function(store, query) { // can only handle local queries out of the box if (get(query, 'location') !== Ember.Query.LOCAL) { throw Ember.$error('Ember.Fixture data source can only fetch local queries'); } if (!get(query, 'recordType') && !get(query, 'recordTypes')) { throw Ember.$error('Ember.Fixture data source can only fetch queries with one or more record types'); } if (get(this, 'simulateRemoteResponse')) { var self = this; setTimeout(function() { self._fetch(store, query); }, get(this, 'latency')); } else this._fetch(store, query); }, /** @private Actually performs the fetch. */ _fetch: function(store, query) { // NOTE: Assumes recordType or recordTypes is defined. checked in fetch() var recordType = get(query, 'recordType'), recordTypes = get(query, 'recordTypes') || [recordType]; // load fixtures for each recordType recordTypes.forEach(function(recordType) { if (Ember.typeOf(recordType) === 'string') { recordType = getPath(recordType); } if (recordType) this.loadFixturesFor(store, recordType); }, this); // notify that query has now loaded - puts it into a READY state store.dataSourceDidFetchQuery(query); }, // .......................................................... // RETRIEVING // /** @private */ retrieveRecords: function(store, storeKeys) { // first let's see if the fixture data source can handle any of the // storeKeys var latency = get(this, 'latency'), ret = this.hasFixturesFor(storeKeys) ; if (!ret) return ret ; if (get(this, 'simulateRemoteResponse')) { var self = this; setTimeout(function() { self._retrieveRecords(store, storeKeys); }, latency); } else this._retrieveRecords(store, storeKeys); return ret ; }, _retrieveRecords: function(store, storeKeys) { storeKeys.forEach(function(storeKey) { var ret = [], recordType = Ember.Store.recordTypeFor(storeKey), id = store.idFor(storeKey), hash = this.fixtureForStoreKey(store, storeKey); ret.push(storeKey); store.dataSourceDidComplete(storeKey, hash, id); }, this); }, // .......................................................... // UPDATE // /** @private */ updateRecords: function(store, storeKeys, params) { // first let's see if the fixture data source can handle any of the // storeKeys var latency = get(this, 'latency'), ret = this.hasFixturesFor(storeKeys) ; if (!ret) return ret ; if (get(this, 'simulateRemoteResponse')) { var self = this; setTimeout(function() { self._updateRecords(store, storeKeys); }, latency); } else this._updateRecords(store, storeKeys); return ret ; }, _updateRecords: function(store, storeKeys) { storeKeys.forEach(function(storeKey) { var hash = store.readDataHash(storeKey); this.setFixtureForStoreKey(store, storeKey, hash); store.dataSourceDidComplete(storeKey); }, this); }, // .......................................................... // CREATE RECORDS // /** @private */ createRecords: function(store, storeKeys, params) { // first let's see if the fixture data source can handle any of the // storeKeys var latency = get(this, 'latency'); if (get(this, 'simulateRemoteResponse')) { var self = this; setTimeout(function() { self._createRecords(store, storeKeys); }, latency); } else this._createRecords(store, storeKeys); return YES ; }, _createRecords: function(store, storeKeys) { storeKeys.forEach(function(storeKey) { var id = store.idFor(storeKey), recordType = store.recordTypeFor(storeKey), dataHash = store.readDataHash(storeKey), fixtures = this.fixturesFor(recordType); if (!id) id = this.generateIdFor(recordType, dataHash, store, storeKey); this._invalidateCachesFor(recordType, storeKey, id); fixtures[id] = dataHash; store.dataSourceDidComplete(storeKey, null, id); }, this); }, // .......................................................... // DESTROY RECORDS // /** @private */ destroyRecords: function(store, storeKeys, params) { // first let's see if the fixture data source can handle any of the // storeKeys var latency = get(this, 'latency'), ret = this.hasFixturesFor(storeKeys) ; if (!ret) return ret ; if (get(this, 'simulateRemoteResponse')) { var self; setTimeout(function() { self._destroyRecords(store, storeKeys); }, latency); } else this._destroyRecords(store, storeKeys); return ret ; }, _destroyRecords: function(store, storeKeys) { storeKeys.forEach(function(storeKey) { var id = store.idFor(storeKey), recordType = store.recordTypeFor(storeKey), fixtures = this.fixturesFor(recordType); this._invalidateCachesFor(recordType, storeKey, id); if (id) delete fixtures[id]; store.dataSourceDidDestroy(storeKey); }, this); }, // .......................................................... // INTERNAL METHODS/PRIMITIVES // /** Load fixtures for a given fetchKey into the store and push it to the ret array. @param {Ember.Store} store the store to load into @param {Ember.Record} recordType the record type to load @param {Ember.Array} ret is passed, array to add loaded storeKeys to. @returns {Ember.Fixture} receiver */ loadFixturesFor: function(store, recordType, ret) { var hashes = [], dataHashes, i, storeKey ; dataHashes = this.fixturesFor(recordType); for(i in dataHashes){ storeKey = recordType.storeKeyFor(i); if (store.peekStatus(storeKey) === Ember.Record.EMPTY) { hashes.push(dataHashes[i]); } if (ret) ret.push(storeKey); } // only load records that were not already loaded to avoid infinite loops if (hashes && hashes.length>0) store.loadRecords(recordType, hashes); return this ; }, /** Generates an id for the passed record type. You can override this if needed. The default generates a storekey and formats it as a string. @param {Class} recordType Subclass of Ember.Record @param {Hash} dataHash the data hash for the record @param {Ember.Store} store the store @param {Number} storeKey store key for the item @returns {String} */ generateIdFor: function(recordType, dataHash, store, storeKey) { return "@id%@".fmt(Ember.Store.generateStoreKey()); }, /** Based on the storeKey it returns the specified fixtures @param {Ember.Store} store the store @param {Number} storeKey the storeKey @returns {Hash} data hash or null */ fixtureForStoreKey: function(store, storeKey) { var id = store.idFor(storeKey), recordType = store.recordTypeFor(storeKey), fixtures = this.fixturesFor(recordType); return fixtures ? fixtures[id] : null; }, /** Update the data hash fixture for the named store key. @param {Ember.Store} store the store @param {Number} storeKey the storeKey @param {Hash} dataHash @returns {Ember.FixturesDataSource} receiver */ setFixtureForStoreKey: function(store, storeKey, dataHash) { var id = store.idFor(storeKey), recordType = store.recordTypeFor(storeKey), fixtures = this.fixturesFor(recordType); this._invalidateCachesFor(recordType, storeKey, id); fixtures[id] = dataHash; return this ; }, /** Get the fixtures for the passed record type and prepare them if needed. Return cached value when complete. @param {Ember.Record} recordType @returns {Hash} data hashes */ fixturesFor: function(recordType) { // get basic fixtures hash. if (!this._fixtures) this._fixtures = {}; var fixtures = this._fixtures[Ember.guidFor(recordType)]; if (fixtures) return fixtures ; // need to load fixtures. var dataHashes = recordType ? recordType.FIXTURES : null, len = dataHashes ? dataHashes.length : 0, primaryKey = recordType ? get(recordType, 'proto').primaryKey:'guid', idx, dataHash, id ; this._fixtures[Ember.guidFor(recordType)] = fixtures = {} ; for(idx=0;idx0) { if (ret === NO) ret = YES ; } else if (ret === YES) ret = Ember.MIXED_STATE ; } }, this); return ret ; }, /** @private Invalidates any internal caches based on the recordType and optional other parameters. Currently this only invalidates the storeKeyCache used for fetch, but it could invalidate others later as well. @param {Ember.Record} recordType the type of record modified @param {Number} storeKey optional store key @param {String} id optional record id @returns {Ember.FixturesDataSource} receiver */ _invalidateCachesFor: function(recordType, storeKey, id) { var cache = this._storeKeyCache; if (cache) delete cache[Ember.guidFor(recordType)]; return this ; } }); /** Default fixtures instance for use in applications. @property {Ember.FixturesDataSource} */ Ember.Record.fixtures = Ember.FixturesDataSource.create(); })({}); (function(exports) { // ========================================================================== // Project: SproutCore DataStore // Copyright: ©2006-2011 Strobe Inc. and contributors. // Portions ©2008-2011 Apple Inc. All rights reserved. // License: Licensed under MIT license (see license.js) // ========================================================================== })({}); (function(exports) { // ========================================================================== // 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) // ========================================================================== var get = Ember.get, set = Ember.set; /** @class A `RecordArray` wraps an array of `storeKeys` and, optionally, a `Query` object. When you access the items of a `RecordArray`, it will automatically convert the `storeKeys` into actual `Ember.Record` objects that the rest of your application can work with. Normally you do not create `RecordArray`s yourself. Instead, a `RecordArray` is returned when you call `Ember.Store.findAll()`, already properly configured. You can usually just work with the `RecordArray` instance just like any other array. The information below about `RecordArray` internals is only intended for those who need to override this class for some reason to do something special. Internal Notes --- Normally the `RecordArray` behavior is very simple. Any array-like operations will be translated into similar calls onto the underlying array of `storeKeys`. The underlying array can be a real array or it may be a `SparseArray`, which is how you implement incremental loading. If the `RecordArray` is created with an `Ember.Query` object as well (and it almost always will have a `Query` object), then the `RecordArray` will also consult the query for various delegate operations such as determining if the record array should update automatically whenever records in the store changes. It will also ask the `Query` to refresh the `storeKeys` whenever records change in the store. If the `Ember.Query` object has complex matching rules, it might be computationally heavy to match a large dataset to a query. To avoid the browser from ever showing a slow script timer in this scenario, the query matching is by default paced at 100ms. If query matching takes longer than 100ms, it will chunk the work with setTimeout to avoid too much computation to happen in one runloop. @extends Ember.Object @extends Ember.Enumerable @extends Ember.Array @since SproutCore 1.0 */ Ember.RecordArray = Ember.Object.extend(Ember.Enumerable, Ember.Array, Ember.MutableEnumerable, Ember.MutableArray, /** @scope Ember.RecordArray.prototype */ { /** The store that owns this record array. All record arrays must have a store to function properly. NOTE: You **MUST** set this property on the `RecordArray` when creating it or else it will fail. @type Ember.Store */ store: null, /** The `Query` object this record array is based upon. All record arrays **MUST** have an associated query in order to function correctly. You cannot change this property once it has been set. NOTE: You **MUST** set this property on the `RecordArray` when creating it or else it will fail. @type Ember.Query */ query: null, /** The array of `storeKeys` as retrieved from the owner store. @type Ember.Array */ storeKeys: null, /** The current status for the record array. Read from the underlying store. @type Number */ status: Ember.Record.EMPTY, /** The current editable state based on the query. If this record array is not backed by an Ember.Query, it is assumed to be editable. @property @type Boolean */ isEditable: function() { var query = get(this, 'query'); return query ? get(query, 'isEditable') : YES; }.property('query').cacheable(), // .......................................................... // ARRAY PRIMITIVES // /** @private Returned length is a pass-through to the `storeKeys` array. @property */ length: function() { this.flush(); // cleanup pending changes var storeKeys = get(this, 'storeKeys'); return storeKeys ? get(storeKeys, 'length') : 0; }.property('storeKeys').cacheable(), /** @private A cache of materialized records. The first time an instance of Ember.Record is created for a store key at a given index, it will be saved to this array. Whenever the `storeKeys` property is reset, this cache is also reset. @type Array */ _scra_records: null, /** @private Looks up the store key in the `storeKeys array and materializes a records. @param {Number} idx index of the object @return {Ember.Record} materialized record */ objectAt: function(idx) { this.flush(); // cleanup pending if needed var recs = this._scra_records, storeKeys = get(this, 'storeKeys'), store = get(this, 'store'), storeKey, ret ; if (!storeKeys || !store) return undefined; // nothing to do if (recs && (ret=recs[idx])) return ret ; // cached // not in cache, materialize if (!recs) this._scra_records = recs = [] ; // create cache storeKey = storeKeys.objectAt(idx); if (storeKey) { // if record is not loaded already, then ask the data source to // retrieve it if (store.peekStatus(storeKey) === Ember.Record.EMPTY) { store.retrieveRecord(null, null, storeKey); } recs[idx] = ret = store.materializeRecord(storeKey); } return ret ; }, /** @private - optimized forEach loop. */ forEach: function(callback, target) { this.flush(); var recs = this._scra_records, storeKeys = get(this, 'storeKeys'), store = get(this, 'store'), len = storeKeys ? get(storeKeys, 'length') : 0, idx, storeKey, rec; if (!storeKeys || !store) return this; // nothing to do if (!recs) recs = this._scra_records = [] ; if (!target) target = this; for(idx=0;idx=0; }, /** @private Returns the first index where the specified record is found. @param {Ember.Record} record @param {Number} startAt optional starting index @returns {Number} index */ indexOf: function(record, startAt) { if (!(record instanceof Ember.Record)) { Ember.Logger.warn("Using indexOf on %@ with an object that is not an Ember.Record".fmt(record)); return -1; // only takes records } this.flush(); var storeKey = get(record, 'storeKey'), storeKeys = get(this, 'storeKeys'); return storeKeys ? storeKeys.indexOf(storeKey, startAt) : -1; }, /** @private Returns the last index where the specified record is found. @param {Ember.Record} record @param {Number} startAt optional starting index @returns {Number} index */ lastIndexOf: function(record, startAt) { if (!(record instanceof Ember.Record)) { Ember.Logger.warn("Using lastIndexOf on %@ with an object that is not an Ember.Record".fmt(record)); return -1; // only takes records } this.flush(); var storeKey = get(record, 'storeKey'), storeKeys = get(this, 'storeKeys'); return storeKeys ? storeKeys.lastIndexOf(storeKey, startAt) : -1; }, /** Adds the specified record to the record array if it is not already part of the array. Provided for compatibilty with `Ember.Set`. @param {Ember.Record} record @returns {Ember.RecordArray} receiver */ add: function(record) { if (!(record instanceof Ember.Record)) return this ; if (this.indexOf(record)<0) this.pushObject(record); return this ; }, /** Removes the specified record from the array if it is not already a part of the array. Provided for compatibility with `Ember.Set`. @param {Ember.Record} record @returns {Ember.RecordArray} receiver */ remove: function(record) { if (!(record instanceof Ember.Record)) return this ; this.removeObject(record); return this ; }, // .......................................................... // HELPER METHODS // /** Extends the standard Ember.Enumerable implementation to return results based on a Query if you pass it in. @param {Ember.Query} query a Ember.Query object @param {Object} target the target object to use @returns {Ember.RecordArray} */ find: function(query, target) { if (query && query.isQuery) { return get(this, 'store').find(query.queryWithScope(this)); } else return this._super(query, target); }, /** Call whenever you want to refresh the results of this query. This will notify the data source, asking it to refresh the contents. @returns {Ember.RecordArray} receiver */ refresh: function() { get(this, 'store').refreshQuery(get(this, 'query')); return this; }, /** Will recompute the results based on the `Ember.Query` attached to the record array. Useful if your query is based on computed properties that might have changed. Use `refresh()` instead of you want to trigger a fetch on your data source since this will purely look at records already loaded into the store. @returns {Ember.RecordArray} receiver */ reload: function() { this.flush(YES); return this; }, /** Destroys the record array. Releases any `storeKeys`, and deregisters with the owner store. @returns {Ember.RecordArray} receiver */ destroy: function() { if (!get(this, 'isDestroyed')) { get(this, 'store').recordArrayWillDestroy(this); } this._super(); }, // .......................................................... // STORE CALLBACKS // // **NOTE**: `storeWillFetchQuery()`, `storeDidFetchQuery()`, // `storeDidCancelQuery()`, and `storeDidErrorQuery()` are tested implicitly // through the related methods in `Ember.Store`. We're doing it this way // because eventually this particular implementation is likely to change; // moving some or all of this code directly into the store. -CAJ /** @private Called whenever the store initiates a refresh of the query. Sets the status of the record array to the appropriate status. @param {Ember.Query} query @returns {Ember.RecordArray} receiver */ storeWillFetchQuery: function(query) { var status = get(this, 'status'), K = Ember.Record; if ((status === K.EMPTY) || (status === K.ERROR)) status = K.BUSY_LOADING; if (status & K.READY) status = K.BUSY_REFRESH; set(this, 'status', status); return this ; }, /** @private Called whenever the store has finished fetching a query. @param {Ember.Query} query @returns {Ember.RecordArray} receiver */ storeDidFetchQuery: function(query) { set(this, 'status', Ember.Record.READY_CLEAN); return this ; }, /** @private Called whenever the store has cancelled a refresh. Sets the status of the record array to the appropriate status. @param {Ember.Query} query @returns {Ember.RecordArray} receiver */ storeDidCancelQuery: function(query) { var status = get(this, 'status'), K = Ember.Record; if (status === K.BUSY_LOADING) status = K.EMPTY; else if (status === K.BUSY_REFRESH) status = K.READY_CLEAN; set(this, 'status', status); return this ; }, /** @private Called whenever the store encounters an error while fetching. Sets the status of the record array to the appropriate status. @param {Ember.Query} query @returns {Ember.RecordArray} receiver */ storeDidErrorQuery: function(query) { set(this, 'status', Ember.Record.ERROR); return this ; }, /** @private Called by the store whenever it changes the state of certain store keys. If the receiver cares about these changes, it will mark itself as dirty and add the changed store keys to the _scq_changedStoreKeys index set. The next time you try to access the record array, it will call `flush()` and add the changed keys to the underlying `storeKeys` array if the new records match the conditions of the record array's query. @param {Ember.Array} storeKeys the effected store keys @param {Ember.Set} recordTypes the record types for the storeKeys. @returns {Ember.RecordArray} receiver */ storeDidChangeStoreKeys: function(storeKeys, recordTypes) { var query = get(this, 'query'); // fast path exits if (get(query, 'location') !== Ember.Query.LOCAL) return this; if (!query.containsRecordTypes(recordTypes)) return this; // ok - we're interested. mark as dirty and save storeKeys. var changed = this._scq_changedStoreKeys; if (!changed) changed = this._scq_changedStoreKeys = Ember.IndexSet.create(); changed.addEach(storeKeys); set(this, 'needsFlush', YES); if (get(this, 'storeKeys')) { this.flush(); } return this; }, /** Applies the query to any pending changed store keys, updating the record array contents as necessary. This method is called automatically anytime you access the RecordArray to make sure it is up to date, but you can call it yourself as well if you need to force the record array to fully update immediately. Currently this method only has an effect if the query location is `Ember.Query.LOCAL`. You can call this method on any `RecordArray` however, without an error. @param {Boolean} _flush to force it - use reload() to trigger it @returns {Ember.RecordArray} receiver */ flush: function(_flush) { // Are we already inside a flush? If so, then don't do it again, to avoid // never-ending recursive flush calls. Instead, we'll simply mark // ourselves as needing a flush again when we're done. if (this._insideFlush) { set(this, 'needsFlush', YES); return this; } if (!get(this, 'needsFlush') && !_flush) return this; // nothing to do set(this, 'needsFlush', NO); // avoid running again. // fast exit var query = get(this, 'query'), store = get(this, 'store'); if (!store || !query || get(query, 'location') !== Ember.Query.LOCAL) { return this; } this._insideFlush = YES; // OK, actually generate some results var storeKeys = get(this, 'storeKeys'), changed = this._scq_changedStoreKeys, didChange = NO, K = Ember.Record, storeKeysToPace = [], startDate = new Date(), rec, status, recordType, sourceKeys, scope, included; // if we have storeKeys already, just look at the changed keys var oldStoreKeys = storeKeys; if (storeKeys && !_flush) { if (changed) { changed.forEach(function(storeKey) { if(storeKeysToPace.length>0 || new Date()-startDate>Ember.RecordArray.QUERY_MATCHING_THRESHOLD) { storeKeysToPace.push(storeKey); return; } // get record - do not include EMPTY or DESTROYED records status = store.peekStatus(storeKey); if (!(status & K.EMPTY) && !((status & K.DESTROYED) || (status === K.BUSY_DESTROYING))) { rec = store.materializeRecord(storeKey); included = !!(rec && query.contains(rec)); } else included = NO ; // if storeKey should be in set but isn't -- add it. if (included) { if (storeKeys.indexOf(storeKey)<0) { if (!didChange) storeKeys = storeKeys.copy(); storeKeys.pushObject(storeKey); } // if storeKey should NOT be in set but IS -- remove it } else { if (storeKeys.indexOf(storeKey)>=0) { if (!didChange) storeKeys = storeKeys.copy(); storeKeys.removeObject(storeKey); } // if (storeKeys.indexOf) } // if (included) }, this); // make sure resort happens didChange = YES ; } // if (changed) //console.log(this.toString() + ' partial flush took ' + (new Date()-startDate) + ' ms'); // if no storeKeys, then we have to go through all of the storeKeys // and decide if they belong or not. ick. } else { // collect the base set of keys. if query has a parent scope, use that if (scope = get(query, 'scope')) { sourceKeys = get(scope.flush(), 'storeKeys'); // otherwise, lookup all storeKeys for the named recordType... } else if (recordType = get(query, 'expandedRecordTypes')) { sourceKeys = Ember.IndexSet.create(); recordType.forEach(function(cur) { sourceKeys.addEach(store.storeKeysFor(recordType)); }); } // loop through storeKeys to determine if it belongs in this query or // not. storeKeys = []; sourceKeys.forEach(function(storeKey) { if(storeKeysToPace.length>0 || new Date()-startDate>Ember.RecordArray.QUERY_MATCHING_THRESHOLD) { storeKeysToPace.push(storeKey); return; } status = store.peekStatus(storeKey); if (!(status & K.EMPTY) && !((status & K.DESTROYED) || (status === K.BUSY_DESTROYING))) { rec = store.materializeRecord(storeKey); if (rec && query.contains(rec)) storeKeys.push(storeKey); } }); //console.log(this.toString() + ' full flush took ' + (new Date()-startDate) + ' ms'); didChange = YES ; } // if we reach our threshold of pacing we need to schedule the rest of the // storeKeys to also be updated if(storeKeysToPace.length>0) { var self = this; // use setTimeout here to guarantee that we hit the next runloop, // and not the same runloop which the invoke* methods do not guarantee window.setTimeout(function() { Ember.run(function() { if(!self || get(self, 'isDestroyed')) return; set(self, 'needsFlush', YES); self._scq_changedStoreKeys = Ember.IndexSet.create().addEach(storeKeysToPace); self.flush(); }); }, 1); } // clear set of changed store keys if (changed) changed.clear(); // only resort and update if we did change if (didChange) { // storeKeys must be a new instance because orderStoreKeys() works on it if (storeKeys && (storeKeys===oldStoreKeys)) { storeKeys = storeKeys.copy(); } storeKeys = Ember.Query.orderStoreKeys(storeKeys, query, store); if (Ember.compare(oldStoreKeys, storeKeys) !== 0){ set(this, 'storeKeys', Ember.copy(storeKeys)); // replace content } } this._insideFlush = NO; return this; }, /** Set to `YES` when the query is dirty and needs to update its storeKeys before returning any results. `RecordArray`s always start dirty and become clean the first time you try to access their contents. @type Boolean */ needsFlush: YES, // .......................................................... // EMULATE Ember.StoreError API // /** Returns `YES` whenever the status is `Ember.Record.ERROR`. This will allow you to put the UI into an error state. @property @type Boolean */ isError: function() { return get(this, 'status') & Ember.Record.ERROR; }.property('status').cacheable(), /** Returns the receiver if the record array is in an error state. Returns `null` otherwise. @property @type Ember.Record */ errorValue: function() { return get(this, 'isError') ? Ember.val(get(this, 'errorObject')) : null ; }.property('isError').cacheable(), /** Returns the current error object only if the record array is in an error state. If no explicit error object has been set, returns `Ember.Record.GENERIC_ERROR.` @property @type Ember.StoreError */ errorObject: function() { if (get(this, 'isError')) { var store = get(this, 'store'); return store.readQueryError(get(this, 'query')) || Ember.Record.GENERIC_ERROR; } else return null ; }.property('isError').cacheable(), // .......................................................... // INTERNAL SUPPORT // propertyWillChange: function(key) { if (key === 'storeKeys') { var storeKeys = get(this, 'storeKeys'); var len = storeKeys ? get(storeKeys, 'length') : 0; this.arrayContentWillChange(0, len, 0); } return this._super(key); }, /** @private Invoked whenever the `storeKeys` array changes. Observes changes. */ _storeKeysDidChange: function() { var storeKeys = get(this, 'storeKeys'); var prev = this._prevStoreKeys, oldLen, newLen, f = this._storeKeysContentDidChange, fs = this._storeKeysStateDidChange; if (storeKeys === prev) { return; } // nothing to do oldLen = prev ? get(prev, 'length') : 0; newLen = storeKeys ? get(storeKeys, 'length') : 0; this._storeKeysContentWillChange(prev, 0, oldLen, newLen); if (prev) { prev.removeArrayObserver(this, { willChange: this._storeKeysContentWillChange, didChange: this._storeKeysContentDidChange }); } this._prevStoreKeys = storeKeys; if (storeKeys) { storeKeys.addArrayObserver(this, { willChange: this._storeKeysContentWillChange, didChange: this._storeKeysContentDidChange }); } this._storeKeysContentDidChange(storeKeys, 0, oldLen, newLen); }.observes('storeKeys'), /** @private If anyone adds an array observer on to the record array, make sure we flush so that the observers don't fire the first time length is calculated. */ addArrayObserver: function() { this.flush(); return this._super.apply(this, arguments); }, _storeKeysContentWillChange: function(target, start, removedCount, addedCount) { this.arrayContentWillChange(start, removedCount, addedCount); }, /** @private Invoked whenever the content of the `storeKeys` array changes. This will dump any cached record lookup and then notify that the enumerable content has changed. */ _storeKeysContentDidChange: function(target, start, removedCount, addedCount) { if (this._scra_records) this._scra_records.length=0 ; // clear cache this.arrayContentDidChange(start, removedCount, addedCount); }, /** @private */ init: function() { this._super(); this._storeKeysDidChange(); } }); Ember.RecordArray.reopenClass(/** @scope Ember.RecordArray.prototype */{ /** Standard error throw when you try to modify a record that is not editable @type Ember.StoreError */ NOT_EDITABLE: Ember.StoreError.desc("Ember.RecordArray is not editable"), /** Number of milliseconds to allow a query matching to run for. If this number is exceeded, the query matching will be paced so as to not lock up the browser (by essentially splitting the work with a setTimeout) @type Number */ QUERY_MATCHING_THRESHOLD: 100 }); })({}); (function(exports) { // ========================================================================== // 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) // ========================================================================== /*globals sc_assert */ var get = Ember.get, set = Ember.set, getPath = Ember.getPath, none = Ember.none; /** @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 Ember.Object @since SproutCore 1.0 */ Ember.Store = Ember.Object.extend( /** @scope Ember.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 Ember.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 {Ember.DataSource|String} dataSource the data source @returns {Ember.Store} receiver */ from: function(dataSource) { set(this, 'dataSource', dataSource); return this ; }, // lazily convert data source to real object _getDataSource: function() { var ret = get(this, 'dataSource'); if (typeof ret === 'string') { ret = getPath( ret); if (ret && ret.isClass) ret = ret.create(); if (ret) set(this, '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 {Ember.DataSource...} dataSource one or more data source arguments @returns {Ember.Store} reciever */ cascade: function(dataSource) { var dataSources = Array.prototype.slice.call(arguments) ; dataSource = Ember.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 Ember.NestedStore) @returns {Ember.NestedStore} new nested store chained to receiver */ chain: function(attrs, newStoreClass) { if (!attrs) attrs = {}; attrs.parentStore = this; if (!newStoreClass) newStoreClass = Ember.NestedStore; // Ensure the passed-in class is a type of nested store. sc_assert("%@ is a valid class".fmt(newStoreClass), Ember.typeOf(newStoreClass) === 'class'); sc_assert("%@ is a type of Ember.NestedStore".fmt(newStoreClass), Ember.NestedStore.detect(newStoreClass)); // Replicate parent records references attrs.childRecords = this.childRecords ? Ember.copy(this.childRecords) : {}; attrs.parentRecords = this.parentRecords ? Ember.copy(this.parentRecords) : {}; var ret = newStoreClass.create(attrs), 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 {Ember.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 {Ember.Store} store store instance @returns {Boolean} YES if belongs */ hasNestedStore: function(store) { while(store && (store !== this)) store = get(store, '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 Ember.Set */ changelog: null, /** An array of `Ember.StoreError` 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 `Ember.StoreError` 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 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]) ? Ember.Store.EDITABLE : Ember.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] = Ember.copy(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 {Ember.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 that = this; this._propagateToChildren(storeKey, function(storeKey){ that.writeDataHash(storeKey, 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 `Ember.RECORD_EMPTY` (assuming you just unloaded the record). If you are deleting the record you may set it to `Ember.Record.DESTROYED_CLEAN`. Be sure to also call `dataHashDidChange()` to register this change. @param {Number} storeKey @param {String} status optional new status @returns {Ember.Store} reciever */ removeDataHash: function(storeKey, status) { // don't use delete -- that will allow parent dataHash to come through this.dataHashes[storeKey] = null; this.statuses[storeKey] = status || Ember.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 `Ember.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] || Ember.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] || Ember.Record.EMPTY; }, /** Writes the current status for a storeKey. If the new status is `Ember.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 {Ember.StoreError} error optional error object @returns {Ember.Store} receiver */ 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 @param {String} key that changed (optional) @returns {Ember.Store} receiver */ dataHashDidChange: function(storeKeys, rev, statusOnly, key) { // update the revision for storeKey. Use generateStoreKey() because that // gaurantees a universally (to this store hierarchy anyway) unique // key value. if (!rev) rev = Ember.Store.generateStoreKey(); var isArray, len, idx, storeKey; isArray = Ember.typeOf(storeKeys) === 'array'; if (isArray) { len = storeKeys.length; } else { len = 1; storeKey = storeKeys; } var that = this; function iter(storeKey){ that.dataHashDidChange(storeKey, null, statusOnly, key); } 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 {Ember.Store} receiver */ reset: function() { // create a new empty data store this.dataHashes = {} ; this.revisions = {} ; this.statuses = {} ; // also reset temporary objects and errors this.chainedChanges = this.locks = this.editables = null; this.changelog = null ; this.recordErrors = null; this.queryErrors = null; var records = this.records, storeKey; if (records) { for(storeKey in records) { if (!records.hasOwnProperty(storeKey)) continue ; this._notifyRecordPropertyChange(parseInt(storeKey, 10), NO); } } set(this, '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 persistant 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 {Ember.Store} nestedStore the child store @param {Ember.Set} changes the set of changed store keys @param {Boolean} force @returns {Ember.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 = Ember.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 {Number} storeKey The storeKey for the dataHash. @returns {Ember.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 = Ember.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 {Ember.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 {Ember.Record} Returns the created record */ createRecord: function(recordType, dataHash, id) { var primaryKey, storeKey, status, K = Ember.Record, changelog, defaultVal, ret, attr; // First, try to get an id. If no id is passed, look it up in the // dataHash. if (!id && (primaryKey = get(recordType, 'proto').primaryKey)) { id = dataHash[primaryKey]; // if still no id, check if there is a defaultValue function for // the primaryKey attribute and assign that attr = Ember.RecordAttribute.attrFor(get(recordType, 'proto'), primaryKey); defaultVal = attr && get(attr, 'defaultValue'); if(!id && Ember.typeOf(defaultVal)==='function') { id = dataHash[primaryKey] = defaultVal(); } } // Next get the storeKey - base on id if available storeKey = id ? recordType.storeKeyFor(id) : Ember.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===Ember.DESTROYED_CLEAN || status===Ember.StoreError)) { throw K.BAD_STATE_ERROR; } // add dataHash and setup initial status -- also save recordType this.writeDataHash(storeKey, (dataHash ? dataHash : {}), K.READY_NEW); Ember.Store.replaceRecordTypeFor(storeKey, recordType); this.dataHashDidChange(storeKey); // Record is now in a committable state -- add storeKey to changelog changelog = this.changelog; if (!changelog) changelog = Ember.Set.create(); changelog.add(storeKey); this.changelog = changelog; // if commit records is enabled if(get(this, 'commitRecordsAutomatically')){ Ember.run.schedule('actions', this, this.commitRecords); } // Finally return materialized record, after we propagate the status to // any aggregrate records. ret = this.materializeRecord(storeKey); 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 {Ember.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 = Ember.typeOf(recordTypes) === '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 (Ember.Record) @param {Array} ids to commit @param {Ember.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 = Ember.typeOf(recordTypes) === 'array', hasCallbackArray = Ember.typeOf(callbacks) === 'array', retCreate= [], retUpdate= [], retDestroy = [], rev = Ember.Store.generateStoreKey(), K = Ember.Record, recordType, idx, storeKey, status, key, 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 ? get(storeKeys, 'length') : (ids ? get(ids, 'length') : 0); for(idx=0;idx0 || params)) { ret = source.commitRecords.call(source, this, retCreate, retUpdate, retDestroy, params); } //remove all commited 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 {Ember.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 additonal 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 {Ember.Record|Array} recordTypes class or array of classes @param {Array} ids ids to destroy @param {Array} storeKeys (optional) store keys to destroy @returns {Ember.Store} the store. */ cancelRecords: function(recordTypes, ids, storeKeys) { var source = this._getDataSource(), isArray = Ember.typeOf(recordTypes) === 'array', K = Ember.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, NO); } 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 {Ember.Query} query the query you cancelled @returns {Ember.Store} receiver */ dataSourceDidCancelQuery: function(query) { return this._scstore_dataSourceDidCancelQuery(query, YES); }, _scstore_dataSourceDidCancelQuery: function(query, createIfNeeded) { var recArray = this._findQuery(query, createIfNeeded, NO), nestedStores = get(this, 'nestedStores'), loc = nestedStores ? get(nestedStores, '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 {Ember.Query} query the query with the error @param {Ember.StoreError} error [optional] an Ember.StoreError instance to associate with query @returns {Ember.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[Ember.guidFor(query)] = error; } return this._scstore_dataSourceDidErrorQuery(query, YES); }, _scstore_dataSourceDidErrorQuery: function(query, createIfNeeded) { var recArray = this._findQuery(query, createIfNeeded, NO), nestedStores = get(this, 'nestedStores'), loc = nestedStores ? get(nestedStores, '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() { this._super(); this.reset(); }, toString: function() { // Include the name if the client has specified one. var name = get(this, 'name'); if (!name) { return this._super(); } else { var ret = this._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 Ember.Store.idFor(storeKey); }, /** Given a storeKey, return the recordType. @param {Number} storeKey the store key @returns {Ember.Record} record instance */ recordTypeFor: function(storeKey) { return Ember.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 {Ember.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 {Ember.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 {Ember.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 Ember.Store.recordTypesByStoreKey) { recType = Ember.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] != Ember.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(); } }) ; Ember.Store.reopenClass(/** @scope Ember.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 `Ember.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 {Ember.Query} query query object */ queryFor: function(storeKey) { return this.queriesByStoreKey[storeKey]; }, /** Given a `storeKey` returns the `Ember.Record` class associated with the key. If no record type is associated with the store key, returns `null`. The Ember.Record class will only be found if you have already called storeKeyFor() on the record. @param {Number} storeKey the store key @returns {Ember.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 {Ember.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 {Ember.Record} recordType a record class @returns {Ember.Store} reciever */ replaceRecordTypeFor: function(storeKey, recordType) { this.recordTypesByStoreKey[storeKey] = recordType; return this ; } }); /** @private */ Ember.Store.reopen({ nextStoreIndex: 1 }); // .......................................................... // COMPATIBILITY // /** @private global store is used only for deprecated compatibility methods. Don't use this in real code. */ Ember.Store._getDefaultStore = function() { var store = this._store; if(!store) this._store = store = Ember.Store.create(); return store; }; /** @private DEPRECATED Included for compatibility, loads data hashes with the named `recordType`. If no `recordType` is passed, expects to find a `recordType` property in the data hashes. `dataSource` and `isLoaded` params are ignored. Calls `Ember.Store#loadRecords()` on the default store. Do not use this method in new code. @param {Array} dataHashes data hashes to import @param {Object} dataSource ignored @param {Ember.Record} recordType default record type @param {Boolean} isLoaded ignored @returns {Array} Ember.Record instances for loaded data hashes */ Ember.Store.updateRecords = function(dataHashes, dataSource, recordType, isLoaded) { Ember.Logger.warn("Ember.Store.updateRecords() is deprecated. Use loadRecords() instead"); var store = this._getDefaultStore(), len = dataHashes.length, idx, ret; // if no recordType was passed, build an array of recordTypes from hashes if (!recordType) { recordType = []; for(idx=0;idx rev)) { this._notifyRecordPropertyChange(parseInt(storeKey, 10)); } } } this.reset(); this.flush(); return this ; }, /** When you are finished working with a chained store, call this method to tear it down. This will also discard any pending changes. @returns {Ember.Store} receiver */ destroy: function() { this.discardChanges(); var parentStore = get(this, 'parentStore'); if (parentStore) parentStore.willDestroyNestedStore(this); this._super(); return this ; }, /** Resets a store's data hash contents to match its parent. */ reset: function() { var nRecords, nr, sk; // requires a pstore to reset var parentStore = get(this, 'parentStore'); if (!parentStore) throw Ember.Store.NO_PARENT_STORE_ERROR; // inherit data store from parent store. this.dataHashes = o_create(parentStore.dataHashes); this.revisions = o_create(parentStore.revisions); this.statuses = o_create(parentStore.statuses); // beget nested records references this.childRecords = parentStore.childRecords ? o_create(parentStore.childRecords) : {}; this.parentRecords = parentStore.parentRecords ? o_create(parentStore.parentRecords) : {}; // also, reset private temporary objects this.chainedChanges = this.locks = this.editables = null; this.changelog = null ; // TODO: Notify record instances set(this, 'hasChanges', NO); }, /** @private Chain to parentstore */ refreshQuery: function(query) { var parentStore = get(this, 'parentStore'); if (parentStore) parentStore.refreshQuery(query); return this ; }, /** Returns the `Ember.StoreError` object associated with a specific record. Delegates the call to the parent store. @param {Number} storeKey The store key of the record. @returns {Ember.StoreError} Ember.StoreError or null if no error associated with the record. */ readError: function(storeKey) { var parentStore = get(this, 'parentStore'); return parentStore ? parentStore.readError(storeKey) : null; }, /** Returns the `Ember.StoreError` object associated with a specific query. Delegates the call to the parent store. @param {Ember.Query} query The Ember.Query with which the error is associated. @returns {Ember.StoreError} Ember.StoreError or null if no error associated with the query. */ readQueryError: function(query) { var parentStore = get(this, 'parentStore'); return parentStore ? parentStore.readQueryError(query) : 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 `INHERITED`, `EDITABLE`, and `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]) ? Ember.Store.EDITABLE : (locks && locks[storeKey]) ? Ember.Store.LOCKED : Ember.Store.INHERITED ; }, /** @private Locks the data hash so that it iterates independently from the parent store. */ _lock: function(storeKey) { var locks = this.locks, rev, editables, pk, pr, path, tup, obj, key; // already locked -- nothing to do if (locks && locks[storeKey]) return this; // create locks if needed if (!locks) locks = this.locks = []; // fixup editables editables = this.editables; if (editables) editables[storeKey] = 0; // if the data hash in the parent store is editable, then clone the hash // for our own use. Otherwise, just copy a reference to the data hash // in the parent store. -- find first non-inherited state var pstore = get(this, 'parentStore'), editState; while(pstore && (editState=pstore.storeKeyEditState(storeKey)) === Ember.Store.INHERITED) { pstore = get(pstore, 'parentStore'); } if (pstore && editState === Ember.Store.EDITABLE) { pk = this.childRecords[storeKey]; if (pk){ // Since this is a nested record we have to actually walk up the // parent chain to get to the root parent and clone that hash. And // then reconstruct the memory space linking. this._lock(pk); pr = this.parentRecords[pk]; if (pr) { path = pr[storeKey]; this.dataHashes[storeKey] = path ? Ember.getPath(this.dataHashes[pk], path) : null; } } else { this.dataHashes[storeKey] = Ember.copy(pstore.dataHashes[storeKey], YES); } if (!editables) editables = this.editables = []; editables[storeKey] = 1 ; // mark as editable } else this.dataHashes[storeKey] = this.dataHashes[storeKey]; // also copy the status + revision this.statuses[storeKey] = this.statuses[storeKey]; rev = this.revisions[storeKey] = this.revisions[storeKey]; // save a lock and make it not editable locks[storeKey] = rev || 1; return this ; }, /** @private - adds chaining support */ readDataHash: function(storeKey) { if (get(this, 'lockOnRead')) this._lock(storeKey); return this.dataHashes[storeKey]; }, /** @private - adds chaining support */ readEditableDataHash: function(storeKey) { // lock the data hash if needed this._lock(storeKey); return this._super(storeKey); }, /** @private - adds chaining support - Does not call sc_super because the implementation of the method vary too much. */ writeDataHash: function(storeKey, hash, status) { var locks = this.locks, didLock = NO, rev ; // Update our dataHash and/or status, depending on what was passed in. // Note that if no new hash was passed in, we'll lock the storeKey to // properly fork our dataHash from our parent store. Similarly, if no // status was passed in, we'll save our own copy of the value. if (hash) { this.dataHashes[storeKey] = hash; } else { this._lock(storeKey); didLock = YES; } if (status) { this.statuses[storeKey] = status; } else { if (!didLock) this.statuses[storeKey] = (this.statuses[storeKey] || Ember.Record.READY_NEW); } if (!didLock) { rev = this.revisions[storeKey] = this.revisions[storeKey]; // copy ref // make sure we lock if needed. if (!locks) locks = this.locks = []; if (!locks[storeKey]) locks[storeKey] = rev || 1; } // Also note that this hash is now editable. (Even if we locked it, // above, it may not have been marked as editable.) var editables = this.editables; if (!editables) editables = this.editables = []; editables[storeKey] = 1 ; // use number for dense array support return this ; }, /** @private - adds chaining support */ removeDataHash: function(storeKey, status) { // record optimistic lock revision var locks = this.locks; if (!locks) locks = this.locks = []; if (!locks[storeKey]) locks[storeKey] = this.revisions[storeKey] || 1; return this._super(storeKey, status); }, /** @private - bookkeeping for a single data hash. */ dataHashDidChange: function(storeKeys, rev, statusOnly, key) { // update the revision for storeKey. Use generateStoreKey() because that // gaurantees a universally (to this store hierarchy anyway) unique // key value. if (!rev) rev = Ember.Store.generateStoreKey(); var isArray, len, idx, storeKey; isArray = Ember.typeOf(storeKeys) === 'array'; if (isArray) { len = storeKeys.length; } else { len = 1; storeKey = storeKeys; } var changes = this.chainedChanges; if (!changes) changes = this.chainedChanges = Ember.Set.create(); for(idx=0;idx0); this.flush(); return this ; }, // .......................................................... // HIGH-LEVEL RECORD API // /** @private - adapt for nested store */ queryFor: function(recordType, conditions, params) { return get(this, 'parentStore').queryFor(recordType, conditions, params); }, /** @private - adapt for nested store */ findAll: function(recordType, conditions, params, recordArray, _store) { if (!_store) _store = this; return get(this, 'parentStore').findAll(recordType, conditions, params, recordArray, _store); }, // .......................................................... // CORE RECORDS API // // The methods in this section can be used to manipulate records without // actually creating record instances. /** @private - adapt for nested store Unlike for the main store, for nested stores if isRefresh=YES, we'll throw an error if the record is dirty. We'll otherwise avoid setting our status because that can disconnect us from upper and/or lower stores. */ retrieveRecords: function(recordTypes, ids, storeKeys, isRefresh) { var pstore = get(this, 'parentStore'), idx, storeKey, newStatus, len = (!storeKeys) ? ids.length : storeKeys.length, K = Ember.Record, status; // Is this a refresh? if (isRefresh) { for(idx=0;idx