Created
August 22, 2011 02:55
-
-
Save LuisSala/1161562 to your computer and use it in GitHub Desktop.
SproutCore DataStore Current as of 8/21/11
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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) | |
// ========================================================================== | |
require('sproutcore-runtime'); | |
require('sproutcore-datastore/system/record'); | |
var get = SC.get, set = SC.set, getPath = SC.getPath; | |
/** | |
@class | |
This permits you to perform queries on your data store, | |
written in a SQL-like language. Here is a simple example: | |
q = SC.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 = SC.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 = SC.Query.create({ | |
conditions: "firstName = 'Jonny' AND lastName = 'Cash'", | |
recordType: MyApp.MyModel, | |
orderBy: "lastName, year DESC" | |
}); | |
The default order direction is ascending. You can change it to descending | |
by writing `'DESC'` 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: | |
SC.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: | |
SC.Query.registerComparison(property_name, comparison_for_this_property); | |
Examples | |
Some example queries: | |
TODO add examples | |
@extends SC.Object | |
@extends SC.Copyable | |
@extends SC.Freezable | |
@since SproutCore 1.0 | |
*/ | |
SC.Query = SC.Object.extend(SC.Copyable, SC.Freezable, | |
/** @scope SC.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 `"DESC "` or `"ASC "` 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 SC.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 SC.Enumerable | |
*/ | |
recordTypes: null, | |
/** | |
Returns the complete set of `recordType`s matched by this query. Includes | |
any named `recordType`s plus their subclasses. | |
@property | |
@type SC.Enumerable | |
*/ | |
expandedRecordTypes: function() { | |
var ret = SC.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(SC.Record, ret); | |
// save in queue. if a new recordtype is defined, we will be notified. | |
q = SC.Query._scq_queriesWithExpandedRecordTypes; | |
if (!q) { | |
q = SC.Query._scq_queriesWithExpandedRecordTypes = SC.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 (SC.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: | |
- `SC.Query.LOCAL` -- indicates that the query results will be | |
automatically computed from the in-memory store. | |
- `SC.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 `SC.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', // SC.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 SC.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') === SC.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') === SC.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 {SC.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 {SC.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 {SC.Record} record1 the first record | |
@param {SC.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 SC.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 (SC.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 (SC.Query.comparisons[propertyName]) { | |
result = SC.Query.comparisons[propertyName]( | |
get(record1, propertyName),get(record2, propertyName)); | |
// if not use default SC.compare() | |
} else { | |
result = SC.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 SC.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 {SC.RecordArray} recordArray the scope | |
@returns {SC.Query} new query | |
*/ | |
queryWithScope: function(recordArray) { | |
// look for a cached query on record array. | |
var key = '__query__'+SC.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 `SC.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 SC.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 SC.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 !SC.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 SC.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 SC.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 SC.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 SC.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 = SC.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<all.length ) { | |
if ( value == all[i] ) found = true; | |
i++; | |
} | |
return found; | |
} | |
} | |
}, | |
'ANY': { | |
reservedWord: true, | |
leftType: 'PRIMITIVE', | |
rightType: 'PRIMITIVE', | |
evalType: 'BOOLEAN', | |
/** @ignore */ | |
evaluate: function (r,w) { | |
var prop = this.leftSide.evaluate(r,w); | |
var values = this.rightSide.evaluate(r,w); | |
var found = false; | |
var i = 0; | |
while ( found===false && i<values.length ) { | |
if ( prop == values[i] ) found = true; | |
i++; | |
} | |
return found; | |
} | |
}, | |
'MATCHES': { | |
reservedWord: true, | |
leftType: 'PRIMITIVE', | |
rightType: 'PRIMITIVE', | |
evalType: 'BOOLEAN', | |
/** @ignore */ | |
evaluate: function (r,w) { | |
var toMatch = this.leftSide.evaluate(r,w); | |
var matchWith = this.rightSide.evaluate(r,w); | |
return matchWith.test(toMatch); | |
} | |
}, | |
'TYPE_IS': { | |
reservedWord: true, | |
rightType: 'PRIMITIVE', | |
evalType: 'BOOLEAN', | |
/** @ignore */ | |
evaluate: function (r,w) { | |
var actualType = SC.Store.recordTypeFor(r.storeKey); | |
var right = this.rightSide.evaluate(r,w); | |
var expectType = getPath( right); | |
return actualType == expectType; | |
} | |
}, | |
'null': { | |
reservedWord: true, | |
evalType: 'PRIMITIVE', | |
/** @ignore */ | |
evaluate: function (r,w) { return null; } | |
}, | |
'undefined': { | |
reservedWord: true, | |
evalType: 'PRIMITIVE', | |
/** @ignore */ | |
evaluate: function (r,w) { return undefined; } | |
}, | |
'false': { | |
reservedWord: true, | |
evalType: 'PRIMITIVE', | |
/** @ignore */ | |
evaluate: function (r,w) { return false; } | |
}, | |
'true': { | |
reservedWord: true, | |
evalType: 'PRIMITIVE', | |
/** @ignore */ | |
evaluate: function (r,w) { return true; } | |
}, | |
'YES': { | |
reservedWord: true, | |
evalType: 'PRIMITIVE', | |
/** @ignore */ | |
evaluate: function (r,w) { return true; } | |
}, | |
'NO': { | |
reservedWord: true, | |
evalType: 'PRIMITIVE', | |
/** @ignore */ | |
evaluate: function (r,w) { return false; } | |
} | |
}, | |
// .......................................................... | |
// TOKENIZER | |
// | |
/** | |
Takes a string and tokenizes it based on the grammar definition | |
provided. Called by `parse()`. | |
@param {String} inputString the string to tokenize | |
@param {Object} grammar the grammar definition (normally queryLanguage) | |
@returns {Array} list of tokens | |
*/ | |
tokenizeString: function (inputString, grammar) { | |
var tokenList = [], | |
c = null, | |
t = null, | |
token = null, | |
tokenType = null, | |
currentToken = null, | |
currentTokenType = null, | |
currentTokenValue = null, | |
currentDelimeter = null, | |
endOfString = false, | |
endOfToken = false, | |
belongsToToken = false, | |
skipThisCharacter = false, | |
rememberCount = {}; | |
// helper function that adds tokens to the tokenList | |
function addToken (tokenType, tokenValue) { | |
t = grammar[tokenType]; | |
//tokenType = t.tokenType; | |
// handling of special cases | |
// check format | |
if (t.format && !t.format.test(tokenValue)) tokenType = "UNKNOWN"; | |
// delimeted token (e.g. by ") | |
if (t.delimeted) skipThisCharacter = true; | |
// reserved words | |
if ( !t.delimeted ) { | |
for ( var anotherToken in grammar ) { | |
if ( grammar[anotherToken].reservedWord | |
&& anotherToken == tokenValue ) { | |
tokenType = anotherToken; | |
} | |
} | |
} | |
// reset t | |
t = grammar[tokenType]; | |
// remembering count type | |
if ( t && t.rememberCount ) { | |
if (!rememberCount[tokenType]) rememberCount[tokenType] = 0; | |
tokenValue = rememberCount[tokenType]; | |
rememberCount[tokenType] += 1; | |
} | |
// push token to list | |
tokenList.push( {tokenType: tokenType, tokenValue: tokenValue} ); | |
// and clean up currentToken | |
currentToken = null; | |
currentTokenType = null; | |
currentTokenValue = null; | |
} | |
// stepping through the string: | |
if (!inputString) return []; | |
var iStLength = inputString.length; | |
for (var i=0; i < iStLength; i++) { | |
// end reached? | |
endOfString = (i===iStLength-1); | |
// current character | |
c = inputString.charAt(i); | |
// set true after end of delimeted token so that | |
// final delimeter is not catched again | |
skipThisCharacter = false; | |
// if currently inside a token | |
if ( currentToken ) { | |
// some helpers | |
t = grammar[currentToken]; | |
endOfToken = t.delimeted ? c===currentDelimeter : t.notAllowed.test(c); | |
// if still in token | |
if ( !endOfToken ) currentTokenValue += c; | |
// if end of token reached | |
if (endOfToken || endOfString) { | |
addToken(currentToken, currentTokenValue); | |
} | |
// if end of string don't check again | |
if ( endOfString && !endOfToken ) skipThisCharacter = true; | |
} | |
// if not inside a token, look for next one | |
if ( !currentToken && !skipThisCharacter ) { | |
// look for matching tokenType | |
for ( token in grammar ) { | |
t = grammar[token]; | |
if (t.firstCharacter && t.firstCharacter.test(c)) { | |
currentToken = token; | |
} | |
} | |
// if tokenType found | |
if ( currentToken ) { | |
t = grammar[currentToken]; | |
currentTokenValue = c; | |
// handling of special cases | |
if ( t.delimeted ) { | |
currentTokenValue = ""; | |
if ( t.lastCharacter ) currentDelimeter = t.lastCharacter; | |
else currentDelimeter = c; | |
} | |
if ( t.singleCharacter || endOfString ) { | |
addToken(currentToken, currentTokenValue); | |
} | |
} | |
} | |
} | |
return tokenList; | |
}, | |
// .......................................................... | |
// BUILD TOKEN TREE | |
// | |
/** | |
Takes an array of tokens and returns a tree, depending on the | |
specified tree logic. The returned object will have an error property | |
if building of the tree failed. Check it to get some information | |
about what happend. | |
If everything worked, the tree can be evaluated by calling | |
tree.evaluate(record, parameters) | |
If `tokenList` is empty, a single token will be returned which will | |
evaluate to true for all records. | |
@param {Array} tokenList the list of tokens | |
@param {Object} treeLogic the logic definition (normally queryLanguage) | |
@returns {Object} token tree | |
*/ | |
buildTokenTree: function (tokenList, treeLogic) { | |
var l = tokenList.slice(); | |
var i = 0; | |
var openParenthesisStack = []; | |
var shouldCheckAgain = false; | |
var error = []; | |
// empty tokenList is a special case | |
if (!tokenList || tokenList.length === 0) { | |
return { evaluate: function(){ return true; } }; | |
} | |
// some helper functions | |
function tokenLogic (position) { | |
var p = position; | |
if ( p < 0 ) return false; | |
var tl = treeLogic[l[p].tokenType]; | |
if ( ! tl ) { | |
error.push("logic for token '"+l[p].tokenType+"' is not defined"); | |
return false; | |
} | |
// save evaluate in token, so that we don't have | |
// to look it up again when evaluating the tree | |
l[p].evaluate = tl.evaluate; | |
return tl; | |
} | |
function expectedType (side, position) { | |
var p = position; | |
var tl = tokenLogic(p); | |
if ( !tl ) return false; | |
if (side == 'left') return tl.leftType; | |
if (side == 'right') return tl.rightType; | |
} | |
function evalType (position) { | |
var p = position; | |
var tl = tokenLogic(p); | |
if ( !tl ) return false; | |
else return tl.evalType; | |
} | |
function removeToken (position) { | |
l.splice(position, 1); | |
if ( position <= i ) i--; | |
} | |
function preceedingTokenExists (position) { | |
var p = position || i; | |
if ( p > 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 (SC.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] == 'DESC') o[i].descending = true; | |
} | |
return o; | |
} | |
} | |
}); | |
// Class Methods | |
SC.Query.reopenClass( /** @scope SC.Query */ { | |
/** | |
Constant used for `SC.Query#location` | |
@type String | |
*/ | |
LOCAL: 'local', | |
/** | |
Constant used for `SC.Query#location` | |
@type String | |
*/ | |
REMOTE: 'remote', | |
/** | |
Given a query, returns the associated `storeKey`. For the inverse of this | |
method see `SC.Store.queryFor()`. | |
@param {SC.Query} query the query | |
@returns {Number} a storeKey. | |
*/ | |
storeKeyFor: function(query) { | |
return query ? get(query, 'storeKey') : null; | |
}, | |
/** | |
Will find which records match a give `SC.Query` and return an array of | |
store keys. This will also apply the sorting for the query. | |
@param {SC.Query} query to apply | |
@param {SC.RecordArray} records to search within | |
@param {SC.Store} store to materialize record from | |
@returns {Array} array instance of store keys matching the SC.Query (sorted) | |
*/ | |
containsRecords: function(query, records, store) { | |
var ret = []; | |
for(var idx=0,len=get(records, 'length');idx<len;idx++) { | |
var record = records.objectAt(idx); | |
if(record && query.contains(record)) { | |
ret.push(get(record, 'storeKey')); | |
} | |
} | |
ret = SC.Query.orderStoreKeys(ret, query, store); | |
return ret; | |
}, | |
/** | |
Sorts a set of store keys according to the orderBy property | |
of the `SC.Query`. | |
@param {Array} storeKeys to sort | |
@param {SC.Query} query to use for sorting | |
@param {SC.Store} store to materialize records from | |
@returns {Array} sorted store keys. may be same instance as passed value | |
*/ | |
orderStoreKeys: function(storeKeys, query, store) { | |
// apply the sort if there is one | |
if (storeKeys) { | |
var res = storeKeys.sort(function(a, b) { | |
return SC.Query.compareStoreKeys(query, store, a, b); | |
}); | |
} | |
return storeKeys; | |
}, | |
/** | |
Default sort method that is used when calling `containsStoreKeys()` | |
or `containsRecords()` on this query. Simply materializes two records | |
based on `storekey`s before passing on to `compare()`. | |
@param {Number} storeKey1 a store key | |
@param {Number} storeKey2 a store key | |
@returns {Number} -1 if record1 < record2, +1 if record1 > 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 `SC.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 `SC.Query.local()` and `SC.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 = SC.Query.local(Ab.Person); | |
var peopleAndCompanies = SC.Query.local([Ab.Person, Ab.Company]); | |
var people = SC.Query.local('Ab.Person'); | |
var peopleAndCompanies = SC.Query.local('Ab.Person Ab.Company'.w()); | |
var allRecords = SC.Query.local(SC.Record); | |
The following will match a particular type of condition: | |
var married = SC.Query.local(Ab.Person, "isMarried=YES"); | |
var married = SC.Query.local(Ab.Person, "isMarried=%@", [YES]); | |
var married = SC.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 = SC.Query.local(Ab.Person, { orderBy: "firstName" }); | |
@param {String} location the query location. | |
@param {SC.Record|Array} recordType the record type or types. | |
@param {String} conditions optional conditions | |
@param {Hash} params optional params. or pass multiple args. | |
@returns {SC.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 = SC.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 = SC.Query._scq_recordTypeCache; | |
if (!tmp) tmp = SC.Query._scq_recordTypeCache = {}; | |
cache = tmp[location]; | |
if (!cache) cache = tmp[location] = {}; | |
if (recordType.isEnumerable) { | |
key = recordType.map(function(k) { return SC.guidFor(k); }); | |
key = key.sort().join(':'); | |
} else key = SC.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] = SC.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 = SC.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 `SC.Query.build()`. | |
@param {SC.Record|Array} recordType the record type or types. | |
@param {String} conditions optional conditions | |
@param {Hash} params optional params. or pass multiple args. | |
@returns {SC.Query} | |
*/ | |
local: function(recordType, conditions, params) { | |
return this.build(SC.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 `SC.Query.build()`. | |
@param {SC.Record|Array} recordType the record type or types. | |
@param {String} conditions optional conditions | |
@param {Hash} params optional params. or pass multiple args. | |
@returns {SC.Query} | |
*/ | |
remote: function(recordType, conditions, params) { | |
return this.build(SC.Query.REMOTE, recordType, conditions, params); | |
}, | |
/** @private | |
called by `SC.Record.extend()`. invalidates `expandedRecordTypes` | |
*/ | |
_scq_didDefineRecordType: function() { | |
var q = SC.Query._scq_queriesWithExpandedRecordTypes; | |
if (q) { | |
q.forEach(function(query) { | |
SC.propertyWillChange(query, 'expandedRecordTypes'); | |
SC.propertyDidChange(query, 'expandedRecordTypes'); | |
}, this); | |
q.clear(); | |
} | |
} | |
}); | |
/** @private | |
Hash of registered comparisons by propery name. | |
*/ | |
SC.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 {SC.Query} receiver | |
*/ | |
SC.Query.registerComparison = function(propertyName, comparison) { | |
SC.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 {SC.Query} receiver | |
*/ | |
SC.Query.registerQueryExtension = function(tokenName, token) { | |
get(SC.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 SC | |
require('sproutcore-runtime'); | |
var get = SC.get, set = SC.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 SC.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 SC.ok(obj) on your object will return NO. | |
If isError is YES, then SC.val(obj) will return your errorValue property | |
instead of the receiver. | |
@extends SC.Object | |
@since SproutCore 1.0 | |
*/ | |
SC.StoreError = SC.Object.extend( | |
/** @scope SC.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 SC.StoreError | |
*/ | |
errorObject: function() { | |
return this; | |
}.property().cacheable(), | |
/** | |
Human readable name of the item with the error. | |
@type String | |
*/ | |
label: null, | |
/** @private */ | |
toString: function() { | |
return "SC.StoreError:%@:%@ (%@)".fmt(SC.guidFor(this), get(this, 'message'), get(this, 'code')); | |
}, | |
/** | |
Walk like a duck. | |
@type Boolean | |
*/ | |
isError: YES | |
}) ; | |
/** | |
Creates a new SC.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 {SC.StoreError} new error instance. | |
*/ | |
SC.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 SC.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 {SC.StoreError} new error instance. | |
*/ | |
SC.$error = function(description, label, value, c) { | |
return SC.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} | |
*/ | |
SC.ok = function(ret) { | |
return (ret !== false) && !(ret && ret.isError); | |
}; | |
/** @private */ | |
SC.$ok = SC.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 | |
*/ | |
SC.val = function(obj) { | |
if (obj && obj.isError) { | |
return get(obj, 'errorValue') ; // Error has no value | |
} else return obj ; | |
}; | |
/** @private */ | |
SC.$val = SC.val; | |
// STANDARD ERROR OBJECTS | |
/** | |
Standard error code for errors that do not support multiple values. | |
@type Number | |
*/ | |
SC.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) | |
// ========================================================================== | |
require('sproutcore-runtime'); | |
require('sproutcore-datastore/system/query'); | |
require('sproutcore-datastore/system/store_error'); | |
var get = SC.get, set = SC.set, none = SC.none, copy = SC.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 SC.Object | |
@see SC.RecordAttribute | |
@since SproutCore 1.0 | |
*/ | |
SC.Record = SC.Object.extend( | |
/** @scope SC.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 SC.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: | |
`SC.Record.EMPTY`, `SC.Record.BUSY`, `SC.Record.READY`, | |
`SC.Record.DESTROYED`, `SC.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 SC.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') & SC.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 | |
`SC.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') & SC.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 {SC.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 {SC.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){ | |
SC.propertyWillChange(this, 'status'); | |
store.destroyRecord(null, null, sk); | |
SC.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 {SC.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 {SC.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 {SC.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 {SC.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')) { | |
SC.propertyWillChange(this, 'id'); // Reset computed value | |
SC.Store.replaceIdFor(storeKey, value) ; | |
SC.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 = SC.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 = SC.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 = SC.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 {SC.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<len;++idx) { | |
key = aggregates[idx]; | |
val = get(this, key); | |
recs = val instanceof SC.ManyArray ? val : [val]; | |
recs.forEach(iter, this); | |
} | |
}, | |
/** @private used internally to notify of property changes. */ | |
notifyPropertyChange: function(keyName) { | |
SC.propertyWillChange(this, keyName); | |
SC.propertyDidChange(this, keyName); | |
}, | |
/** | |
Called by the store whenever the underlying data hash has changed. This | |
will notify any observers interested in data hash properties that they | |
have changed. | |
@param {Boolean} statusOnly changed | |
@param {String} key that changed (optional) | |
@returns {SC.Record} receiver | |
*/ | |
storeDidChangeProperties: function(statusOnly, keys) { | |
// TODO: Should this function call propagateToAggregates() at the | |
// appropriate times? | |
if (statusOnly) { | |
this.notifyPropertyChange('status'); | |
} else { | |
if (!keys) { | |
keys = get(this, 'store').readDataHash(get(this, 'storeKey')); | |
if (keys) keys = SC.keys(keys); | |
} | |
SC.beginPropertyChanges(this); | |
if (keys) { | |
keys.forEach(function(k) { | |
this.notifyPropertyChange(k); | |
}, this); | |
} | |
this.notifyPropertyChange('status'); | |
SC.endPropertyChanges(this); | |
// also notify manyArrays | |
var manyArrays = this.relationships, | |
loc = manyArrays ? manyArrays.length : 0 ; | |
while(--loc>=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 {SC.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 = SC.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 = SC.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 SC.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 = SC.Store.recordTypeFor(storeKey); | |
if(recordType.ignoreUnknownProperties===YES) { | |
this[key] = value; | |
return value; | |
} | |
// if we're modifying the PKEY, then `SC.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) { | |
SC.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 {SC.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 || (SC.none(recordOnly) && SC.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 SC.StoreError API | |
// | |
/** | |
Returns `YES` whenever the status is SC.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') & SC.Record.ERROR; | |
}.property('status').cacheable(), | |
/** | |
Returns the receiver if the record is in an error state. Returns null | |
otherwise. | |
@type SC.Record | |
@property | |
@dependsOn isError | |
*/ | |
errorValue: function() { | |
return get(this, 'isError') ? SC.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 SC.Record.GENERIC_ERROR. | |
@type SC.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 | |
`SC.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 {SC.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 SC.copy() -- we'll be careful not to edit anything. | |
var attrs = get(this, 'store').readDataHash(get(this, 'storeKey')); | |
return "%@(%@) %@".fmt(this.constructor.toString(), SC.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 SC.Record) { | |
if(prop.match(/[A-Z_]$/) && SC.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 {SC.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 (SC.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 SC.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 `SC.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 {SC.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 = SC.typeOf(value); | |
if (t === 'instance' || t === 'object') { | |
// Get the record type. | |
childNS = get(this, 'nestedRecordNamespace'); | |
if (get(value, 'type') && !SC.none(childNS)) { | |
recordType = get(childNS, get(value, 'type')); | |
} | |
} | |
// Maybe it's not a hash or there was no type property. | |
if (!recordType && key) { | |
attr = SC.RecordAttribute.attrFor(this, key); | |
if (attr) recordType = get(attr, 'typeClass'); | |
} | |
// When all else fails throw and exception. | |
if (!SC.Record.detect(recordType)) { | |
throw 'SC.Child: Error during transform: Invalid record type.'; | |
} | |
return recordType; | |
}, | |
/** | |
Creates a new nested record instance. | |
@param {SC.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 {SC.Record} the nested record created | |
*/ | |
createNestedRecord: function(recordType, hash) { | |
var store, id, sk, pk, cr = null, existingId = null; | |
SC.run(this, function() { | |
hash = hash || {}; // init if needed | |
existingId = hash[get(recordType, 'proto').primaryKey]; | |
store = get(this, 'store'); | |
if (SC.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 (SC.none(id)){ | |
sk = get(cr, 'storeKey'); | |
id = 'cr'+sk; | |
SC.Store.replaceIdFor(sk, id); | |
hash = store.readEditableDataHash(sk); | |
hash[pk] = id; | |
} | |
} | |
} | |
// ID processing if necessary | |
if (SC.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 {SC.Record} childRecord | |
@returns {String} the id generated | |
*/ | |
generateIdForChild: function(childRecord){} | |
}) ; | |
// Class Methods | |
SC.Record.reopenClass( /** @scope SC.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 `SC.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 SC.StoreError | |
*/ | |
BAD_STATE_ERROR: SC.$error("Internal Inconsistency"), | |
/** | |
Error for when you try to create a new record that already exists. | |
@static | |
@constant | |
@type SC.StoreError | |
*/ | |
RECORD_EXISTS_ERROR: SC.$error("Record Exists"), | |
/** | |
Error for when you attempt to locate a record that is not found | |
@static | |
@constant | |
@type SC.StoreError | |
*/ | |
NOT_FOUND_ERROR: SC.$error("Not found "), | |
/** | |
Error for when you try to modify a record that is currently busy | |
@static | |
@constant | |
@type SC.StoreError | |
*/ | |
BUSY_ERROR: SC.$error("Busy"), | |
/** | |
Generic unknown record error | |
@static | |
@constant | |
@type SC.StoreError | |
*/ | |
GENERIC_ERROR: SC.$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 `SC.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 SC.Record subclasses. | |
MyApp.Contact = SC.Record.extend({ | |
firstName: SC.Record.attr(String, { isRequired: YES }) | |
}); | |
@param {Class} type the attribute type | |
@param {Hash} opts the options for the attribute | |
@returns {SC.RecordAttribute} created instance | |
*/ | |
attr: function(type, opts) { | |
return SC.RecordAttribute.attr(type, opts).computed(); | |
}, | |
/** | |
Returns an `SC.RecordAttribute` that describes a fetched attribute. When | |
you reference this attribute, it will return an `SC.RecordArray` that uses | |
the type as the fetch key and passes the attribute value as a param. | |
Use this helper when you define SC.Record subclasses. | |
MyApp.Group = SC.Record.extend({ | |
contacts: SC.Record.fetch('MyApp.Contact') | |
}); | |
@param {SC.Record|String} recordType The type of records to load | |
@param {Hash} opts the options for the attribute | |
@returns {SC.RecordAttribute} created instance | |
*/ | |
fetch: function(recordType, opts) { | |
return SC.FetchedAttribute.attr(recordType, opts).computed(); | |
}, | |
/** | |
Will return one of the following: | |
1. `SC.ManyAttribute` that describes a record array backed by an | |
array of guids stored in the underlying JSON. | |
2. `SC.ChildrenAttribute` that describes a record array backed by a | |
array of hashes. | |
You can edit the contents of this relationship. | |
For `SC.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 {SC.Record|String} recordType The type of record to create | |
@param {Hash} opts the options for the attribute | |
@returns {SC.ManyAttribute|SC.ChildrenAttribute} created instance | |
*/ | |
toMany: function(recordType, opts) { | |
opts = opts || {}; | |
var isNested = opts.nested || opts.isNested; | |
var attr; | |
if(isNested){ | |
attr = SC.ChildrenAttribute.attr(recordType, opts); | |
} | |
else { | |
attr = SC.ManyAttribute.attr(recordType, opts); | |
} | |
return attr.computed(); | |
}, | |
/** | |
Will return one of the following: | |
1. `SC.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. `SC.ChildAttribute` that you can edit the contents | |
of this relationship. | |
@param {SC.Record|String} recordType the type of the record to create | |
@param {Hash} opts additional options | |
@returns {SC.SingleAttribute|SC.ChildAttribute} created instance | |
*/ | |
toOne: function(recordType, opts) { | |
opts = opts || {}; | |
var isNested = opts.nested || opts.isNested; | |
var attr; | |
if(isNested){ | |
attr = SC.ChildAttribute.attr(recordType, opts); | |
} | |
else { | |
attr = SC.SingleAttribute.attr(recordType, opts); | |
} | |
return attr.computed(); | |
}, | |
/** | |
Returns all storeKeys mapped by Id for this record type. This method is | |
used mostly by the `SC.Store` and the Record to coordinate. You will | |
rarely need to call this method yourself. | |
@returns {Hash} | |
*/ | |
storeKeysById: function() { | |
var key = 'storeKey-'+SC.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 `SC.Store.idFor()` and | |
`SC.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 = SC.Store.generateStoreKey(); | |
SC.Store.idsByStoreKey[ret] = id ; | |
SC.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 {SC.Store} store the store | |
@param {String} id the record id or a query | |
@returns {SC.Record} record instance | |
*/ | |
find: function(store, id) { | |
return store.find(this, id); | |
}, | |
/** @private - enhance extend to notify SC.Query as well. */ | |
extend: function() { | |
var ret = SC.Object.extend.apply(this, arguments); | |
// Clear aggregates cache when creating a new subclass | |
// of SC.Record | |
ret.aggregates = null; | |
SC.Query._scq_didDefineRecordType(ret); | |
return ret ; | |
} | |
}) ; | |
K = SC.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) | |
// ========================================================================== | |
require('sproutcore-runtime'); | |
require('sproutcore-datastore/system/record'); | |
require('sproutcore-datetime'); | |
var get = SC.get, set = SC.set, getPath = SC.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 SC.Record, you can configure it this way: | |
title: SC.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 SC.Record subclass: | |
relatedToComputed: SC.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 SC.Record. | |
@extends SC.Object | |
@see SC.Record | |
@see SC.ManyAttribute | |
@see SC.SingleAttribute | |
@since SproutCore 1.0 | |
*/ | |
SC.RecordAttribute = SC.Object.extend( | |
/** @scope SC.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 (SC.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 = SC.RecordAttribute.transforms, | |
ret ; | |
// walk up class hierarchy looking for a transform handler | |
while(klass && !(ret = transforms[SC.guidFor(klass)])) { | |
// check if super has create property to detect SC.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 `SC.RecordAttribute.transforms` hash. See | |
SC.RecordAttribute.registerTransform() for more. | |
@param {SC.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(!SC.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++) SC.addObserver(value, children[i], this, this._SCRA_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 `SC.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 | |
*/ | |
_SCRA_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 `SC.RecordAttribute.transforms` hash. See | |
`SC.RecordAttribute.registerTransform()` for more. | |
@param {SC.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 `SC.RecordAttribute` uses `isProperty` to masquerade | |
as a computed property. Get expects a property be a function, thus we | |
need to implement call. | |
@param {SC.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 (SC.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__" + SC.guidFor(this) ; | |
this.lastSetValueKey = "__lastValue__" + SC.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 = SC.computed(function(key, value) { | |
return attr.call(this, key, value); | |
}); | |
ret.attr = attr; | |
return ret ; | |
} | |
}) ; | |
// .......................................................... | |
// CLASS METHODS | |
// | |
SC.RecordAttribute.reopenClass( | |
/** @scope SC.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 {SC.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 SC.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 {SC.RecordAttribute} receiver | |
*/ | |
registerTransform: function(klass, transform) { | |
SC.RecordAttribute.transforms[SC.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 {SC.Record} rec record instance to inspect | |
@param {String} keyName key name to retrieve | |
@returns {SC.RecordAttribute} the attribute or null if none defined | |
*/ | |
attrFor: function(rec, keyName) { | |
var ret = SC.meta(rec, false).descs[keyName]; | |
return ret && ret.attr; | |
} | |
}); | |
// .......................................................... | |
// STANDARD ATTRIBUTE TRANSFORMS | |
// | |
// Object, String, Number just pass through. | |
/** @private - generic converter for Boolean records */ | |
SC.RecordAttribute.registerTransform(Boolean, { | |
/** @private - convert an arbitrary object value to a boolean */ | |
to: function(obj) { | |
return SC.none(obj) ? null : !!obj; | |
} | |
}); | |
/** @private - generic converter for Numbers */ | |
SC.RecordAttribute.registerTransform(Number, { | |
/** @private - convert an arbitrary object value to a Number */ | |
to: function(obj) { | |
return SC.none(obj) ? null : Number(obj) ; | |
} | |
}); | |
/** @private - generic converter for Strings */ | |
SC.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') && !SC.none(obj) && obj.toString) { | |
obj = obj.toString(); | |
} | |
return obj; | |
} | |
}); | |
/** @private - generic converter for Array */ | |
SC.RecordAttribute.registerTransform(Array, { | |
/** @private - check if obj is an array | |
*/ | |
to: function(obj) { | |
if (!SC.isArray(obj) && !SC.none(obj)) { | |
obj = []; | |
} | |
return obj; | |
}, | |
observesChildren: ['[]'] | |
}); | |
/** @private - generic converter for Object */ | |
SC.RecordAttribute.registerTransform(Object, { | |
/** @private - check if obj is an object */ | |
to: function(obj) { | |
if (!(typeof obj === 'object') && !SC.none(obj)) { | |
obj = {}; | |
} | |
return obj; | |
} | |
}); | |
/** @private - generic converter for SC.Record-type records */ | |
SC.RecordAttribute.registerTransform(SC.Record, { | |
/** @private - convert a record id to a record instance */ | |
to: function(id, attr, recordType, parentRecord) { | |
var store = get(parentRecord, 'store'); | |
if (SC.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 */ | |
SC.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 */ | |
SC.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 (SC.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 (SC.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 (SC.DateTime && !SC.RecordAttribute.transforms[SC.guidFor(SC.DateTime)]) { | |
/** | |
Registers a transform to allow `SC.DateTime` to be used as a record | |
attribute, ie `SC.Record.attr(SC.DateTime);` | |
Because `SC.RecordAttribute` is in the datastore framework and | |
`SC.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.` | |
*/ | |
SC.RecordAttribute.registerTransform(SC.DateTime, { | |
/** @private | |
Convert a String to a DateTime | |
*/ | |
to: function(str, attr) { | |
if (SC.none(str) || (str instanceof SC.DateTime)) return str; | |
if (SC.none(str) || (str instanceof Date)) return SC.DateTime.create(str.getTime()); | |
var format = get(attr, 'format'); | |
return SC.DateTime.parse(str, format ? format : SC.DateTime.recordFormat); | |
}, | |
/** @private | |
Convert a DateTime to a String | |
*/ | |
from: function(dt, attr) { | |
if (SC.none(dt)) return dt; | |
var format = get(attr, 'format'); | |
return dt.toFormattedString(format ? format : SC.DateTime.recordFormat); | |
} | |
}); | |
} | |
/** | |
Parses a coreset represented as an array. | |
*/ | |
SC.RecordAttribute.registerTransform(SC.Set, { | |
to: function(value, attr, type, record, key) { | |
return SC.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) | |
// ========================================================================== | |
require('sproutcore-datastore/system/record'); | |
require('sproutcore-datastore/attributes/record_attribute'); | |
var get = SC.get, set = SC.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 `SC.Record` objects. | |
There are many ways you can configure a ManyAttribute: | |
contacts: SC.ChildAttribute.attr('SC.Child'); | |
@extends SC.RecordAttribute | |
@since SproutCore 1.0 | |
*/ | |
SC.ChildAttribute = SC.RecordAttribute.extend( | |
/** @scope SC.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 'SC.Child: Error during transform: Unable to retrieve parent record.'; | |
} | |
if (!SC.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 (SC.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 {SC.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__'+SC.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 (SC.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 = SC.get, set = SC.set, getPath = SC.getPath; | |
/** | |
@class | |
A `ChildArray` is used to map an array of `ChildRecord` objects. | |
@extends SC.Enumerable | |
@extends SC.Array | |
@since SproutCore 1.0 | |
*/ | |
SC.ChildArray = SC.Object.extend(SC.Enumerable, SC.Array, SC.MutableEnumerable, SC.MutableArray, | |
/** @scope SC.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 {SC.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 {SC.Array} | |
*/ | |
children: null, | |
/** | |
The store that owns this record array. All record arrays must have a | |
store to function properly. | |
@type SC.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 SC.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 {SC.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 {SC.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 {SC.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 {SC.Array} recs records to be converted to hashes. | |
@returns {SC.Array} array of hashes. | |
*/ | |
_processRecordsToHashes: function(recs){ | |
var store, sk; | |
recs = recs || []; | |
recs.forEach( function(me, idx) { | |
if (me instanceof SC.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 {SC.Array} keys optional | |
@returns {SC.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) | |
// ========================================================================== | |
require('sproutcore-runtime'); | |
require('sproutcore-datastore/system/record'); | |
require('sproutcore-datastore/attributes/record_attribute'); | |
require('sproutcore-datastore/attributes/child_attribute'); | |
require('sproutcore-datastore/system/child_array'); | |
var get = SC.get, set = SC.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 SC.Record objects. | |
There are many ways you can configure a ChildrenAttribute: | |
contacts: SC.ChildrenAttribute.attr('SC.Child'); | |
@extends SC.RecordAttribute | |
@since SproutCore 1.0 | |
*/ | |
SC.ChildrenAttribute = SC.ChildAttribute.extend( | |
/** @scope SC.ChildrenAttribute.prototype */ { | |
// .......................................................... | |
// LOW-LEVEL METHODS | |
// | |
/** @private - adapted for to many relationship */ | |
toType: function(record, key, value) { | |
var attrKey = get(this, 'key') || key, | |
arrayKey = '__kidsArray__'+SC.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 = SC.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__'+SC.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) | |
// ========================================================================== | |
require('sproutcore-datastore/attributes/record_attribute'); | |
var get = SC.get, set = SC.set, attrFor = SC.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 SC.Enumerable | |
@extends SC.Array | |
@since SproutCore 1.0 | |
*/ | |
SC.ManyArray = SC.Object.extend(SC.Enumerable, SC.MutableEnumerable, SC.MutableArray, SC.Array, | |
/** @scope SC.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 SC.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 SC.ManyAttribute | |
*/ | |
manyAttribute: null, | |
/** | |
The store that owns this record array. All record arrays must have a | |
store to function properly. | |
@type SC.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 SC.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 {SC.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) === SC.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;i<len;i++) ids[i] = get(recs.objectAt(i), 'id'); | |
// if we have an inverse - collect the list of records we are about to | |
// remove | |
inverse = get(this, 'inverse'); | |
if (inverse && amt>0) { | |
toRemove = SC.ManyArray._toRemove; | |
if (toRemove) SC.ManyArray._toRemove = null; // reuse if possible | |
else toRemove = []; | |
for(i=0;i<amt;i++) toRemove[i] = this.objectAt(idx + i); | |
} | |
// pass along - if allowed, this should trigger the content observer | |
storeIds.replace(idx, amt, ids); | |
// ok, notify records that were removed then added; this way reordered | |
// objects are added and removed | |
if (inverse) { | |
// notive removals | |
for(i=0;i<amt;i++) { | |
inverseRecord = toRemove[i]; | |
attr = inverseRecord ? attrFor(inverseRecord, inverse) : null; | |
if (attr && attr.inverseDidRemoveRecord) { | |
attr.inverseDidRemoveRecord(inverseRecord, inverse, record, pname); | |
} | |
} | |
if (toRemove) { | |
toRemove.length = 0; // cleanup | |
if (!SC.ManyArray._toRemove) SC.ManyArray._toRemove = toRemove; | |
} | |
// notify additions | |
for(i=0;i<len;i++) { | |
inverseRecord = recs.objectAt(i); | |
attr = inverseRecord ? attrFor(inverseRecord, inverse) : null; | |
if (attr && attr.inverseDidAddRecord) { | |
attr.inverseDidAddRecord(inverseRecord, inverse, record, pname); | |
} | |
} | |
} | |
// only mark record dirty if there is no inverse or we are master | |
if (record && (!inverse || get(this, 'isMaster'))) { | |
record.recordDidChange(pname); | |
} | |
this.enumerableContentDidChange(idx, amt, len - amt); | |
return this; | |
}, | |
// .......................................................... | |
// INVERSE SUPPORT | |
// | |
/** | |
Called by the `ManyAttribute` whenever a record is removed on the inverse | |
of the relationship. | |
@param {SC.Record} inverseRecord the record that was removed | |
@returns {SC.ManyArray} receiver | |
*/ | |
removeInverseRecord: function(inverseRecord) { | |
if (!inverseRecord) return this; // nothing to do | |
var id = get(inverseRecord, 'id'), | |
storeIds = get(this, 'editableStoreIds'), | |
idx = (storeIds && id) ? storeIds.indexOf(id) : -1, | |
record; | |
if (idx >= 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 {SC.Record} inverseRecord the record this array is a part of | |
@returns {SC.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 = SC.typeOf(orderBy), | |
ret, idx, len; | |
if (t === 'function') ret = orderBy(a, b); | |
else if (t === 'string') ret = SC.compare(a,b); | |
else { | |
len = get(orderBy, 'length'); | |
ret = 0; | |
for(idx=0;(ret===0) && (idx<len);idx++) ret = SC.compare(a,b); | |
} | |
return ret ; | |
}, | |
// .......................................................... | |
// INTERNAL SUPPORT | |
// | |
/** @private | |
Invoked whenever the `storeIds` array changes. Observes changes. | |
*/ | |
recordPropertyDidChange: function(keys) { | |
if (keys && !keys.contains(get(this, 'propertyName'))) return this; | |
var storeIds = get(this, 'readOnlyStoreIds'), oldLen, newLen; | |
var prev = this._prevStoreIds, f = this._storeIdsContentDidChange; | |
if (storeIds === prev) return this; // nothing to do | |
if (prev) { | |
prev.removeArrayObserver(this, { | |
willChange: this.arrayWillChange, | |
didChange: f | |
}); | |
oldLen = get(prev, 'length'); | |
} else { | |
oldLen = 0; | |
} | |
if (storeIds) { | |
if(!storeIds.get("hasArrayObservers")) { | |
storeIds.addArrayObserver(this, { | |
willChange: this.arrayWillChange, | |
didChange: f | |
}); | |
} | |
newLen = get(storeIds, 'length'); | |
} else { | |
newLen = 0; | |
} | |
this.arrayContentWillChange(0, oldLen, newLen); | |
this._prevStoreIds = storeIds; | |
this._storeIdsContentDidChange(null, 0, oldLen, newLen); | |
}, | |
arrayWillChange: function(item, start, removedCount, addedCount) { | |
this.arrayContentWillChange(start, removedCount, addedCount); | |
}, | |
/** @private | |
Invoked whenever the content of the storeIds array changes. This will | |
dump any cached record lookup and then notify that the enumerable content | |
has changed. | |
*/ | |
_storeIdsContentDidChange: function(item, 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: ©2006-2011 Strobe Inc. and contributors. | |
// Portions ©2008-2011 Apple Inc. All rights reserved. | |
// License: Licensed under MIT license (see license.js) | |
// ========================================================================== | |
require('sproutcore-datastore/system/record'); | |
require('sproutcore-datastore/attributes/record_attribute'); | |
require('sproutcore-datastore/system/many_array'); | |
var get = SC.get, set = SC.set; | |
/** @class | |
ManyAttribute is a subclass of `RecordAttribute` and handles to-many | |
relationships. | |
When setting ( `set()` ) the value of a `toMany` attribute, make sure | |
to pass in an array of `SC.Record` objects. | |
There are many ways you can configure a `ManyAttribute`: | |
contacts: SC.Record.toMany('MyApp.Contact', { | |
inverse: 'group', // set the key used to represent the inverse | |
isMaster: YES|NO, // indicate whether changing this should dirty | |
transform: function(), // transforms value <=> storeKey, | |
isEditable: YES|NO, make editable or not, | |
through: 'taggings' // set a relationship this goes through | |
}); | |
@extends SC.RecordAttribute | |
@since SproutCore 1.0 | |
*/ | |
SC.ManyAttribute = SC.RecordAttribute.extend( | |
/** @scope SC.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__'+SC.guidFor(this), | |
ret = record[arrayKey], | |
rel; | |
// lazily create a ManyArray one time. after that always return the | |
// same object. | |
if (!ret) { | |
ret = SC.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(!SC.isArray(value)) throw "Expects toMany attribute to be an array"; | |
var len = get(value, 'length'); | |
for(var i=0;i<len;i++) { | |
ret[i] = get(value.objectAt(i), 'id'); | |
} | |
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. | |
You should never call this directly. | |
@param {SC.Record} the record owning this attribute | |
@param {String} key the key for this attribute | |
@param {SC.Record} inverseRecord record that was removed from inverse | |
@param {String} key key on inverse that was modified | |
@returns {void} | |
*/ | |
inverseDidRemoveRecord: function(record, key, inverseRecord, inverseKey) { | |
var manyArray = get(record, key); | |
if (manyArray) { | |
manyArray.removeInverseRecord(inverseRecord); | |
} | |
}, | |
/** | |
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. | |
You should never call this directly. | |
@param {SC.Record} the record owning this attribute | |
@param {String} key the key for this attribute | |
@param {SC.Record} inverseRecord record that was added to inverse | |
@param {String} key key on inverse that was modified | |
@returns {void} | |
*/ | |
inverseDidAddRecord: function(record, key, inverseRecord, inverseKey) { | |
var manyArray = get(record, key); | |
if (manyArray) { | |
manyArray.addInverseRecord(inverseRecord); | |
} | |
} | |
}); | |
})({}); | |
(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) | |
// ========================================================================== | |
require('sproutcore-datastore/system/record'); | |
require('sproutcore-datastore/attributes/record_attribute'); | |
var get = SC.get, set = SC.set; | |
var RecordAttribute_call = get(SC.RecordAttribute, 'proto').call; | |
var attrFor = SC.RecordAttribute.attrFor; | |
/** @class | |
`SingleAttribute` is a subclass of `RecordAttribute` and handles to-one | |
relationships. | |
There are many ways you can configure a `SingleAttribute`: | |
group: SC.Record.toOne('MyApp.Group', { | |
inverse: 'contacts', // set the key used to represent the inverse | |
isMaster: YES|NO, // indicate whether changing this should dirty | |
transform: function(), // transforms value <=> storeKey, | |
isEditable: YES|NO, make editable or not | |
}); | |
@extends SC.RecordAttribute | |
@since SproutCore 1.0 | |
*/ | |
SC.SingleAttribute = SC.RecordAttribute.extend( | |
/** @scope SC.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 SC.Record)) { | |
throw "%@ is not an instance of SC.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 {SC.Record} record the record owning this attribute | |
@param {String} key the key for this attribute | |
@param {SC.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 {SC.Record} record the record owning this attribute | |
@param {String} key the key for this attribute | |
@param {SC.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) | |
// ========================================================================== | |
require('sproutcore-datastore/attributes/child_attribute'); | |
require('sproutcore-datastore/attributes/children_attribute'); | |
require('sproutcore-datastore/attributes/many_attribute'); | |
require('sproutcore-datastore/attributes/record_attribute'); | |
require('sproutcore-datastore/attributes/single_attribute'); | |
})({}); | |
(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) | |
// ========================================================================== | |
require('sproutcore-runtime'); | |
/** | |
Indicates a value has a mixed state of both on and off. | |
@property {String} | |
*/ | |
SC.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 `SC.DataSource` | |
and implement the necessary data source methods to communicate with the | |
particular backend. | |
## Create a Data Source | |
To implement the data source, subclass `SC.DataSource` in a file located | |
either in the root level of your app or framework, or in a directory | |
called "data_sources": | |
MyApp.DataSource = SC.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: SC.Store.create().from(SC.Record.fixtures) | |
// to... | |
store: SC.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. | |
- `SC.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 `SC.Record.EMPTY`. | |
* `SC.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 `SC.Record` | |
subclass itself, not on an instance of `SC.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 SC.Object | |
@since SproutCore 1.0 | |
*/ | |
SC.DataSource = SC.Object.extend( /** @scope SC.DataSource.prototype */ { | |
// .......................................................... | |
// SC.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 SC.Store#find() with a query or SC.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 SC.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 | |
SC.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 SC.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 SC.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 SC.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 {SC.Store} store the requesting store | |
@param {SC.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 {SC.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 | |
`SC.MIXED_STATE` if you can handle some of them. | |
@param {SC.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 = SC.none(ret) ? uret : (ret === uret) ? ret : SC.MIXED_STATE; | |
} | |
if (destroyStoreKeys.length>0) { | |
dret = this.destroyRecords.call(this, store, destroyStoreKeys, params); | |
ret = SC.none(ret) ? dret : (ret === dret) ? ret : SC.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 | |
`SC.MIXED_STATE` if you can retrieve some of the. | |
@param {SC.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 | |
`SC.MIXED_STATE` if you can handle some of them. | |
@param {SC.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 SC.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 | |
`SC.MIXED_STATE` if you can handle some of them. | |
@param {SC.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 SC.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 | |
`SC.MIXED_STATE` if you can handle some of them. | |
@param {SC.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 SC.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<len;idx++) { | |
idOrParams = ids ? ids[idx] : params; | |
cur = action.call(this, store, storeKeys[idx], idOrParams); | |
if (ret === undefined) { | |
ret = cur ; | |
} else if (ret === YES) { | |
ret = (cur === YES) ? YES : SC.MIXED_STATE ; | |
} else if (ret === NO) { | |
ret = (cur === NO) ? NO : SC.MIXED_STATE ; | |
} | |
} | |
return !SC.none(ret) ? ret : null ; | |
}, | |
// .......................................................... | |
// SINGLE RECORD ACTIONS | |
// | |
/** | |
Called from `updatesRecords()` to update a single record. This is the | |
most basic primitive to can implement to support updating a record. | |
To support cascading data stores, be sure to return `NO` if you cannot | |
handle the passed storeKey or `YES` if you can. | |
@param {SC.Store} store the requesting store | |
@param {Array} storeKey key to update | |
@param {Hash} params to be passed down to data source. originated | |
from the commitRecords() call on the store | |
@returns {Boolean} YES if handled | |
*/ | |
updateRecord: function(store, storeKey, params) { | |
return NO ; | |
}, | |
/** | |
Called from `retrieveRecords()` to retrieve a single record. | |
@param {SC.Store} store the requesting store | |
@param {Array} storeKey key to retrieve | |
@param {String} id the id to retrieve | |
@returns {Boolean} YES if handled | |
*/ | |
retrieveRecord: function(store, storeKey, id) { | |
return NO ; | |
}, | |
/** | |
Called from `createdRecords()` to created a single record. This is the | |
most basic primitive to can implement to support creating a record. | |
To support cascading data stores, be sure to return `NO` if you cannot | |
handle the passed storeKey or `YES` if you can. | |
@param {SC.Store} store the requesting store | |
@param {Array} storeKey key to update | |
@param {Hash} params to be passed down to data source. originated | |
from the commitRecords() call on the store | |
@returns {Boolean} YES if handled | |
*/ | |
createRecord: function(store, storeKey, params) { | |
return NO ; | |
}, | |
/** | |
Called from `destroyRecords()` to destroy a single record. This is the | |
most basic primitive to can implement to support destroying a record. | |
To support cascading data stores, be sure to return `NO` if you cannot | |
handle the passed storeKey or `YES` if you can. | |
@param {SC.Store} store the requesting store | |
@param {Array} storeKey key to update | |
@param {Hash} params to be passed down to data source. originated | |
from the commitRecords() call on the store | |
@returns {Boolean} YES if handled | |
*/ | |
destroyRecord: function(store, storeKey, params) { | |
return NO ; | |
} | |
}); | |
})({}); | |
(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) | |
// ========================================================================== | |
require('sproutcore-runtime'); | |
require('sproutcore-datastore/data_sources/data_source'); | |
var get = SC.get, set = SC.set; | |
/** @class | |
A cascading data source will actually forward requests onto an array of | |
additional data sources, stopping when one of the data sources returns YES, | |
indicating that it handled the request. | |
You can use a cascading data source to tie together multiple data sources, | |
treating them as a single namespace. | |
## Configuring a Cascade Data Source | |
You will usually define your cascading data source in your main method after | |
all the classes you have are loaded. | |
MyApp.dataSource = SC.CascadeDataSource.create({ | |
dataSources: "prefs youtube photos".w(), | |
prefs: MyApp.PrefsDataSource.create({ root: "/prefs" }), | |
youtube: YouTube.YouTubeDataSource.create({ apiKey: "123456" }), | |
photos: MyApp.PhotosDataSource.create({ root: "photos" }) | |
}); | |
set(MyApp.store, 'dataSource', MyApp.dataSource); | |
Note that the order you define your dataSources property will determine the | |
order in which requests will cascade from the store. | |
Alternatively, you can use a more jQuery-like API for defining your data | |
sources: | |
MyApp.dataSource = SC.CascadeDataSource.create() | |
.from(MyApp.PrefsDataSource.create({ root: "/prefs" })) | |
.from(YouTube.YouTubeDataSource.create({ apiKey: "123456" })) | |
.from(MyApp.PhotosDataSource.create({ root: "photos" })); | |
set(MyApp.store, 'dataSource', MyApp.dataSource); | |
In this case, the order you call from() will determine the order the request | |
will cascade. | |
@extends SC.DataSource | |
@since SproutCore 1.0 | |
*/ | |
SC.CascadeDataSource = SC.DataSource.extend( | |
/** @scope SC.CascadeDataSource.prototype */ { | |
/** | |
The data sources used by the cascade, in the order that they are to be | |
followed. Usually when you define the cascade, you will define this | |
array. | |
@property {Array} | |
*/ | |
dataSources: null, | |
/** | |
Add a data source to the list of sources to use when cascading. Used to | |
build the data source cascade effect. | |
@param {SC.DataSource} dataSource a data source instance to add. | |
@returns {SC.CascadeDataSource} receiver | |
*/ | |
from: function(dataSource) { | |
var dataSources = get(this, 'dataSources'); | |
if (!dataSources) set(this, 'dataSources', dataSources = []); | |
dataSources.push(dataSource); | |
return this ; | |
}, | |
// .......................................................... | |
// SC.STORE ENTRY POINTS | |
// | |
/** @private - just cascades */ | |
fetch: function(store, query) { | |
var sources = get(this, 'dataSources'), | |
len = sources ? sources.length : 0, | |
ret = NO, | |
cur, source, idx; | |
for(idx=0; (ret !== YES) && idx<len; idx++) { | |
source = sources.objectAt(idx); | |
cur = source.fetch ? source.fetch.apply(source, arguments) : NO; | |
ret = this._handleResponse(ret, cur); | |
} | |
return ret ; | |
}, | |
/** @private - just cascades */ | |
retrieveRecords: function(store, storeKeys, ids) { | |
var sources = get(this, 'dataSources'), | |
len = sources ? sources.length : 0, | |
ret = NO, | |
cur, source, idx; | |
for(idx=0; (ret !== YES) && idx<len; idx++) { | |
source = sources.objectAt(idx); | |
cur = source.retrieveRecords.apply(source, arguments); | |
ret = this._handleResponse(ret, cur); | |
} | |
return ret ; | |
}, | |
/** @private - just cascades */ | |
commitRecords: function(store, createStoreKeys, updateStoreKeys, destroyStoreKeys, params) { | |
var sources = get(this, 'dataSources'), | |
len = sources ? sources.length : 0, | |
ret = NO, | |
cur, source, idx; | |
for(idx=0; (ret !== YES) && idx<len; idx++) { | |
source = sources.objectAt(idx); | |
cur = source.commitRecords.apply(source, arguments); | |
ret = this._handleResponse(ret, cur); | |
} | |
return ret ; | |
}, | |
/** @private - just cascades */ | |
cancel: function(store, storeKeys) { | |
var sources = get(this, 'dataSources'), | |
len = sources ? sources.length : 0, | |
ret = NO, | |
cur, source, idx; | |
for(idx=0; (ret !== YES) && idx<len; idx++) { | |
source = sources.objectAt(idx); | |
cur = source.cancel.apply(source, arguments); | |
ret = this._handleResponse(ret, cur); | |
} | |
return ret ; | |
}, | |
// .......................................................... | |
// INTERNAL SUPPORT | |
// | |
/** @private */ | |
init: function() { | |
this._super(); | |
// if a dataSources array is defined, look for any strings and lookup | |
// the same on the data source. Replace. | |
var sources = get(this, 'dataSources'), | |
idx = sources ? get(sources, 'length') : 0, | |
source; | |
while(--idx>=0) { | |
source = sources[idx]; | |
if (SC.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 : SC.MIXED_STATE ; | |
else return SC.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) | |
// ========================================================================== | |
require('sproutcore-runtime'); | |
require('sproutcore-datastore/data_sources/data_source'); | |
require('sproutcore-datastore/system/record'); | |
require('sproutcore-datastore/system/store_error'); | |
var get = SC.get, set = SC.set, getPath = SC.getPath; | |
/** @class | |
TODO: Describe Class | |
@extends SC.DataSource | |
@since SproutCore 1.0 | |
*/ | |
SC.FixturesDataSource = SC.DataSource.extend( | |
/** @scope SC.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') !== SC.Query.LOCAL) { | |
throw SC.$error('SC.Fixture data source can only fetch local queries'); | |
} | |
if (!get(query, 'recordType') && !get(query, 'recordTypes')) { | |
throw SC.$error('SC.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 (SC.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 = SC.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 {SC.Store} store the store to load into | |
@param {SC.Record} recordType the record type to load | |
@param {SC.Array} ret is passed, array to add loaded storeKeys to. | |
@returns {SC.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) === SC.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 SC.Record | |
@param {Hash} dataHash the data hash for the record | |
@param {SC.Store} store the store | |
@param {Number} storeKey store key for the item | |
@returns {String} | |
*/ | |
generateIdFor: function(recordType, dataHash, store, storeKey) { | |
return "@id%@".fmt(SC.Store.generateStoreKey()); | |
}, | |
/** | |
Based on the storeKey it returns the specified fixtures | |
@param {SC.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 {SC.Store} store the store | |
@param {Number} storeKey the storeKey | |
@param {Hash} dataHash | |
@returns {SC.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 {SC.Record} recordType | |
@returns {Hash} data hashes | |
*/ | |
fixturesFor: function(recordType) { | |
// get basic fixtures hash. | |
if (!this._fixtures) this._fixtures = {}; | |
var fixtures = this._fixtures[SC.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[SC.guidFor(recordType)] = fixtures = {} ; | |
for(idx=0;idx<len;idx++) { | |
dataHash = dataHashes[idx]; | |
id = dataHash[primaryKey]; | |
if (!id) id = this.generateIdFor(recordType, dataHash); | |
fixtures[id] = dataHash; | |
} | |
return fixtures; | |
}, | |
/** | |
Returns YES if fixtures for a given recordType have already been loaded | |
@param {SC.Record} recordType | |
@returns {Boolean} storeKeys | |
*/ | |
fixturesLoadedFor: function(recordType) { | |
if (!this._fixtures) return NO; | |
var ret = [], fixtures = this._fixtures[SC.guidFor(recordType)]; | |
return fixtures ? YES: NO; | |
}, | |
/** | |
Returns YES or SC.MIXED_STATE if one or more of the storeKeys can be | |
handled by the fixture data source. | |
@param {Array} storeKeys the store keys | |
@returns {Boolean} YES if all handled, MIXED_STATE if some handled | |
*/ | |
hasFixturesFor: function(storeKeys) { | |
var ret = NO ; | |
storeKeys.forEach(function(storeKey) { | |
if (ret !== SC.MIXED_STATE) { | |
var recordType = SC.Store.recordTypeFor(storeKey), | |
fixtures = recordType ? recordType.FIXTURES : null ; | |
if (fixtures && fixtures.length && fixtures.length>0) { | |
if (ret === NO) ret = YES ; | |
} else if (ret === YES) ret = SC.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 {SC.Record} recordType the type of record modified | |
@param {Number} storeKey optional store key | |
@param {String} id optional record id | |
@returns {SC.FixturesDataSource} receiver | |
*/ | |
_invalidateCachesFor: function(recordType, storeKey, id) { | |
var cache = this._storeKeyCache; | |
if (cache) delete cache[SC.guidFor(recordType)]; | |
return this ; | |
} | |
}); | |
/** | |
Default fixtures instance for use in applications. | |
@property {SC.FixturesDataSource} | |
*/ | |
SC.Record.fixtures = SC.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) | |
// ========================================================================== | |
require('sproutcore-datastore/data_sources/cascading'); | |
require('sproutcore-datastore/data_sources/data_source'); | |
require('sproutcore-datastore/data_sources/fixtures'); | |
})({}); | |
(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) | |
// ========================================================================== | |
require('sproutcore-runtime'); | |
require('sproutcore-datastore/system/record'); | |
require('sproutcore-datastore/system/query'); | |
require('sproutcore-indexset'); | |
var get = SC.get, set = SC.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 `SC.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 `SC.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 `SC.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 `SC.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 SC.Object | |
@extends SC.Enumerable | |
@extends SC.Array | |
@since SproutCore 1.0 | |
*/ | |
SC.RecordArray = SC.Object.extend(SC.Enumerable, SC.Array, SC.MutableEnumerable, SC.MutableArray, | |
/** @scope SC.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 SC.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 SC.Query | |
*/ | |
query: null, | |
/** | |
The array of `storeKeys` as retrieved from the owner store. | |
@type SC.Array | |
*/ | |
storeKeys: null, | |
/** | |
The current status for the record array. Read from the underlying | |
store. | |
@type Number | |
*/ | |
status: SC.Record.EMPTY, | |
/** | |
The current editable state based on the query. If this record array is not | |
backed by an SC.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 SC.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 {SC.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) === SC.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<len;idx++) { | |
rec = recs[idx]; | |
if (!rec) { | |
rec = recs[idx] = store.materializeRecord(storeKeys.objectAt(idx)); | |
} | |
callback.call(target, rec, idx, this); | |
} | |
return this; | |
}, | |
/** @private | |
Replaces a range of records starting at a given index with the replacement | |
records provided. The objects to be inserted must be instances of SC.Record | |
and must have a store key assigned to them. | |
Note that most SC.RecordArrays are *not* editable via `replace()`, since they | |
are generated by a rule-based SC.Query. You can check the `isEditable` property | |
before attempting to modify a record array. | |
@param {Number} idx start index | |
@param {Number} amt count of records to remove | |
@param {SC.RecordArray} recs the records that should replace the removed records | |
@returns {SC.RecordArray} receiver, after mutation has occurred | |
*/ | |
replace: function(idx, amt, recs) { | |
this.flush(); // cleanup pending if needed | |
var storeKeys = get(this, 'storeKeys'), | |
len = recs ? get(recs, 'length') : 0, | |
i, keys; | |
if (!storeKeys) throw "Unable to edit an SC.RecordArray that does not have its storeKeys property set."; | |
if (!get(this, 'isEditable')) throw SC.RecordArray.NOT_EDITABLE; | |
// map to store keys | |
keys = [] ; | |
for(i=0;i<len;i++) keys[i] = get(recs.objectAt(i), 'storeKey'); | |
// pass along - if allowed, this should trigger the content observer | |
storeKeys.replace(idx, amt, keys); | |
return this; | |
}, | |
/** | |
Returns YES if the passed can be found in the record array. This is | |
provided for compatibility with SC.Set. | |
@param {SC.Record} record | |
@returns {Boolean} | |
*/ | |
contains: function(record) { | |
return this.indexOf(record)>=0; | |
}, | |
/** @private | |
Returns the first index where the specified record is found. | |
@param {SC.Record} record | |
@param {Number} startAt optional starting index | |
@returns {Number} index | |
*/ | |
indexOf: function(record, startAt) { | |
if (!(record instanceof SC.Record)) { | |
SC.Logger.warn("Using indexOf on %@ with an object that is not an SC.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 {SC.Record} record | |
@param {Number} startAt optional starting index | |
@returns {Number} index | |
*/ | |
lastIndexOf: function(record, startAt) { | |
if (!(record instanceof SC.Record)) { | |
SC.Logger.warn("Using lastIndexOf on %@ with an object that is not an SC.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 `SC.Set`. | |
@param {SC.Record} record | |
@returns {SC.RecordArray} receiver | |
*/ | |
add: function(record) { | |
if (!(record instanceof SC.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 `SC.Set`. | |
@param {SC.Record} record | |
@returns {SC.RecordArray} receiver | |
*/ | |
remove: function(record) { | |
if (!(record instanceof SC.Record)) return this ; | |
this.removeObject(record); | |
return this ; | |
}, | |
// .......................................................... | |
// HELPER METHODS | |
// | |
/** | |
Extends the standard SC.Enumerable implementation to return results based | |
on a Query if you pass it in. | |
@param {SC.Query} query a SC.Query object | |
@param {Object} target the target object to use | |
@returns {SC.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 {SC.RecordArray} receiver | |
*/ | |
refresh: function() { | |
get(this, 'store').refreshQuery(get(this, 'query')); | |
return this; | |
}, | |
/** | |
Will recompute the results based on the `SC.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 {SC.RecordArray} receiver | |
*/ | |
reload: function() { | |
this.flush(YES); | |
return this; | |
}, | |
/** | |
Destroys the record array. Releases any `storeKeys`, and deregisters with | |
the owner store. | |
@returns {SC.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 `SC.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 {SC.Query} query | |
@returns {SC.RecordArray} receiver | |
*/ | |
storeWillFetchQuery: function(query) { | |
var status = get(this, 'status'), | |
K = SC.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 {SC.Query} query | |
@returns {SC.RecordArray} receiver | |
*/ | |
storeDidFetchQuery: function(query) { | |
set(this, 'status', SC.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 {SC.Query} query | |
@returns {SC.RecordArray} receiver | |
*/ | |
storeDidCancelQuery: function(query) { | |
var status = get(this, 'status'), | |
K = SC.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 {SC.Query} query | |
@returns {SC.RecordArray} receiver | |
*/ | |
storeDidErrorQuery: function(query) { | |
set(this, 'status', SC.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 {SC.Array} storeKeys the effected store keys | |
@param {SC.Set} recordTypes the record types for the storeKeys. | |
@returns {SC.RecordArray} receiver | |
*/ | |
storeDidChangeStoreKeys: function(storeKeys, recordTypes) { | |
var query = get(this, 'query'); | |
// fast path exits | |
if (get(query, 'location') !== SC.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 = SC.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 | |
`SC.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 {SC.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') !== SC.Query.LOCAL) { | |
return this; | |
} | |
this._insideFlush = YES; | |
// OK, actually generate some results | |
var storeKeys = get(this, 'storeKeys'), | |
changed = this._scq_changedStoreKeys, | |
didChange = NO, | |
K = SC.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>SC.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 = SC.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>SC.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() { | |
SC.run(function() { | |
if(!self || get(self, 'isDestroyed')) return; | |
set(self, 'needsFlush', YES); | |
self._scq_changedStoreKeys = SC.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 = SC.Query.orderStoreKeys(storeKeys, query, store); | |
if (SC.compare(oldStoreKeys, storeKeys) !== 0){ | |
set(this, 'storeKeys', SC.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 SC.StoreError API | |
// | |
/** | |
Returns `YES` whenever the status is `SC.Record.ERROR`. This will allow | |
you to put the UI into an error state. | |
@property | |
@type Boolean | |
*/ | |
isError: function() { | |
return get(this, 'status') & SC.Record.ERROR; | |
}.property('status').cacheable(), | |
/** | |
Returns the receiver if the record array is in an error state. Returns | |
`null` otherwise. | |
@property | |
@type SC.Record | |
*/ | |
errorValue: function() { | |
return get(this, 'isError') ? SC.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 | |
`SC.Record.GENERIC_ERROR.` | |
@property | |
@type SC.StoreError | |
*/ | |
errorObject: function() { | |
if (get(this, 'isError')) { | |
var store = get(this, 'store'); | |
return store.readQueryError(get(this, 'query')) || SC.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(); | |
} | |
}); | |
SC.RecordArray.reopenClass(/** @scope SC.RecordArray.prototype */{ | |
/** | |
Standard error throw when you try to modify a record that is not editable | |
@type SC.StoreError | |
*/ | |
NOT_EDITABLE: SC.StoreError.desc("SC.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 */ | |
require('sproutcore-datastore/system/record'); | |
require('sproutcore-datastore/system/nested_store'); | |
require('sproutcore-datastore/system/query'); | |
require('sproutcore-datastore/system/record_array'); | |
var get = SC.get, set = SC.set, getPath = SC.getPath, none = SC.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 SC.Object | |
@since SproutCore 1.0 | |
*/ | |
SC.Store = SC.Object.extend( /** @scope SC.Store.prototype */ { | |
/** | |
An (optional) name of the store, which can be useful during debugging, | |
especially if you have multiple nested stores. | |
@type String | |
*/ | |
name: null, | |
/** | |
An array of all the chained stores that current rely on the receiver | |
store. | |
@type Array | |
*/ | |
nestedStores: null, | |
/** | |
The data source is the persistent storage that will provide data to the | |
store and save changes. You normally will set your data source when you | |
first create your store in your application. | |
@type SC.DataSource | |
*/ | |
dataSource: null, | |
/** | |
This type of store is not nested. | |
@default NO | |
@type Boolean | |
*/ | |
isNested: NO, | |
/** | |
This type of store is not nested. | |
@default NO | |
@type Boolean | |
*/ | |
commitRecordsAutomatically: NO, | |
// .......................................................... | |
// DATA SOURCE SUPPORT | |
// | |
/** | |
Convenience method. Sets the current data source to the passed property. | |
This will also set the store property on the dataSource to the receiver. | |
If you are using this from the `core.js` method of your app, you may need to | |
just pass a string naming your data source class. If this is the case, | |
then your data source will be instantiated the first time it is requested. | |
@param {SC.DataSource|String} dataSource the data source | |
@returns {SC.Store} receiver | |
*/ | |
from: function(dataSource) { | |
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 {SC.DataSource...} dataSource one or more data source arguments | |
@returns {SC.Store} reciever | |
*/ | |
cascade: function(dataSource) { | |
var dataSources = Array.prototype.slice.call(arguments) ; | |
dataSource = SC.CascadeDataSource.create({ | |
dataSources: dataSources | |
}); | |
return this.from(dataSource); | |
}, | |
// .......................................................... | |
// STORE CHAINING | |
// | |
/** | |
Returns a new nested store instance that can be used to buffer changes | |
until you are ready to commit them. When you are ready to commit your | |
changes, call `commitChanges()` or `destroyChanges()` and then `destroy()` | |
when you are finished with the chained store altogether. | |
store = MyApp.store.chain(); | |
.. edit edit edit | |
store.commitChanges().destroy(); | |
@param {Hash} attrs optional attributes to set on new store | |
@param {Class} newStoreClass optional the class of the newly-created nested store (defaults to SC.NestedStore) | |
@returns {SC.NestedStore} new nested store chained to receiver | |
*/ | |
chain: function(attrs, newStoreClass) { | |
if (!attrs) attrs = {}; | |
attrs.parentStore = this; | |
if (!newStoreClass) newStoreClass = SC.NestedStore; | |
// Ensure the passed-in class is a type of nested store. | |
sc_assert("%@ is a valid class".fmt(newStoreClass), | |
SC.typeOf(newStoreClass) === 'class'); | |
sc_assert("%@ is a type of SC.NestedStore".fmt(newStoreClass), | |
SC.NestedStore.detect(newStoreClass)); | |
// Replicate parent records references | |
attrs.childRecords = this.childRecords ? SC.copy(this.childRecords) : {}; | |
attrs.parentRecords = this.parentRecords ? SC.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 {SC.Store} receiver | |
*/ | |
willDestroyNestedStore: function(nestedStore) { | |
if (this.nestedStores) { | |
this.nestedStores.removeObject(nestedStore); | |
} | |
return this ; | |
}, | |
/** | |
Used to determine if a nested store belongs directly or indirectly to the | |
receiver. | |
@param {SC.Store} store store instance | |
@returns {Boolean} YES if belongs | |
*/ | |
hasNestedStore: function(store) { | |
while(store && (store !== this)) store = 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 SC.Set | |
*/ | |
changelog: null, | |
/** | |
An array of `SC.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 `SC.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]) ? SC.Store.EDITABLE : SC.Store.LOCKED ; | |
}, | |
/** | |
Returns the data hash for the given `storeKey`. This will also 'lock' | |
the hash so that further edits to the parent store will no | |
longer be reflected in this store until you reset. | |
@param {Number} storeKey key to retrieve | |
@returns {Hash} data hash or null | |
*/ | |
readDataHash: function(storeKey) { | |
return this.dataHashes[storeKey]; | |
}, | |
/** | |
Returns the data hash for the `storeKey`, cloned so that you can edit | |
the contents of the attributes if you like. This will do the extra work | |
to make sure that you only clone the attributes one time. | |
If you use this method to modify data hash, be sure to call | |
`dataHashDidChange()` when you make edits to record the change. | |
@param {Number} storeKey the store key to retrieve | |
@returns {Hash} the attributes hash | |
*/ | |
readEditableDataHash: function(storeKey) { | |
// read the value - if there is no hash just return; nothing to do | |
var ret = this.dataHashes[storeKey]; | |
if (!ret) return ret ; // nothing to do. | |
// clone data hash if not editable | |
var editables = this.editables; | |
if (!editables) editables = this.editables = []; | |
if (!editables[storeKey]) { | |
editables[storeKey] = 1 ; // use number to store as dense array | |
ret = this.dataHashes[storeKey] = SC.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 {SC.Store} receiver | |
*/ | |
writeDataHash: function(storeKey, hash, status) { | |
// update dataHashes and optionally status. | |
if (hash) this.dataHashes[storeKey] = hash; | |
if (status) this.statuses[storeKey] = status ; | |
// also note that this hash is now editable | |
var editables = this.editables; | |
if (!editables) editables = this.editables = []; | |
editables[storeKey] = 1 ; // use number for dense array support | |
var 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 `SC.RECORD_EMPTY` | |
(assuming you just unloaded the record). If you are deleting the record | |
you may set it to `SC.Record.DESTROYED_CLEAN`. | |
Be sure to also call `dataHashDidChange()` to register this change. | |
@param {Number} storeKey | |
@param {String} status optional new status | |
@returns {SC.Store} reciever | |
*/ | |
removeDataHash: function(storeKey, status) { | |
// don't use delete -- that will allow parent dataHash to come through | |
this.dataHashes[storeKey] = null; | |
this.statuses[storeKey] = status || SC.Record.EMPTY; | |
// hash is gone and therefore no longer editable | |
var editables = this.editables; | |
if (editables) editables[storeKey] = 0 ; | |
return this ; | |
}, | |
/** | |
Reads the current status for a storeKey. This will also lock the data | |
hash. If no status is found, returns `SC.RECORD_EMPTY`. | |
@param {Number} storeKey the store key | |
@returns {Number} status | |
*/ | |
readStatus: function(storeKey) { | |
// use readDataHash to handle optimistic locking. this could be inlined | |
// but for now this minimized copy-and-paste code. | |
this.readDataHash(storeKey); | |
return this.statuses[storeKey] || SC.Record.EMPTY; | |
}, | |
/** | |
Reads the current status for the storeKey without actually locking the | |
record. Usually you won't need to use this method. It is mostly used | |
internally. | |
@param {Number} storeKey the store key | |
@returns {Number} status | |
*/ | |
peekStatus: function(storeKey) { | |
return this.statuses[storeKey] || SC.Record.EMPTY; | |
}, | |
/** | |
Writes the current status for a storeKey. If the new status is | |
`SC.Record.ERROR`, you may also pass an optional error object. Otherwise | |
this param is ignored. | |
@param {Number} storeKey the store key | |
@param {String} newStatus the new status | |
@param {SC.StoreError} error optional error object | |
@returns {SC.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 {SC.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 = SC.Store.generateStoreKey(); | |
var isArray, len, idx, storeKey; | |
isArray = SC.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<len;idx++) { | |
if (isArray) storeKey = storeKeys[idx]; | |
this.revisions[storeKey] = rev; | |
this._notifyRecordPropertyChange(storeKey, statusOnly, key); | |
this._propagateToChildren(storeKey, iter); | |
} | |
return this ; | |
}, | |
/** @private | |
Will push all changes to a the recordPropertyChanges property | |
and execute `flush()` once at the end of the runloop. | |
*/ | |
_notifyRecordPropertyChange: function(storeKey, statusOnly, key) { | |
var records = this.records, | |
nestedStores = get(this, 'nestedStores'), | |
K = SC.Store, | |
rec, editState, len, idx, store, status, keys; | |
// pass along to nested stores | |
len = nestedStores ? nestedStores.length : 0 ; | |
for(idx=0;idx<len;idx++) { | |
store = nestedStores[idx]; | |
status = store.peekStatus(storeKey); // important: peek avoids read-lock | |
editState = store.storeKeyEditState(storeKey); | |
// when store needs to propagate out changes in the parent store | |
// to nested stores | |
if (editState === K.INHERITED) { | |
store._notifyRecordPropertyChange(storeKey, statusOnly, key); | |
} else if (status & SC.Record.BUSY) { | |
// make sure nested store does not have any changes before resetting | |
if(get(store, 'hasChanges')) throw K.CHAIN_CONFLICT_ERROR; | |
store.reset(); | |
} | |
} | |
// store info in changes hash and schedule notification if needed. | |
var changes = this.recordPropertyChanges; | |
if (!changes) { | |
changes = this.recordPropertyChanges = | |
{ storeKeys: SC.Set.create(), | |
records: SC.Set.create(), | |
hasDataChanges: SC.Set.create(), | |
propertyForStoreKeys: {} }; | |
} | |
changes.storeKeys.add(storeKey); | |
if (records && (rec=records[storeKey])) { | |
changes.records.push(storeKey); | |
// If there are changes other than just the status we need to record | |
// that information so we do the right thing during the next flush. | |
// Note that if we're called multiple times before flush and one call | |
// has `statusOnly=true` and another has `statusOnly=false`, the flush | |
// will (correctly) operate in `statusOnly=false` mode. | |
if (!statusOnly) changes.hasDataChanges.push(storeKey); | |
// If this is a key specific change, make sure that only those | |
// properties/keys are notified. However, if a previous invocation of | |
// `_notifyRecordPropertyChange` specified that all keys have changed, we | |
// need to respect that. | |
if (key) { | |
if (!(keys = changes.propertyForStoreKeys[storeKey])) { | |
keys = changes.propertyForStoreKeys[storeKey] = SC.Set.create(); | |
} | |
// If it's '*' instead of a set, then that means there was a previous | |
// invocation that said all keys have changed. | |
if (keys !== '*') { | |
keys.add(key); | |
} | |
} | |
else { | |
// Mark that all properties have changed. | |
changes.propertyForStoreKeys[storeKey] = '*'; | |
} | |
} | |
SC.run.once(this, this.flush); | |
return this; | |
}, | |
/** | |
Delivers any pending changes to materialized records. Normally this | |
happens once, automatically, at the end of the RunLoop. If you have | |
updated some records and need to update records immediately, however, | |
you may call this manually. | |
@returns {SC.Store} receiver | |
*/ | |
flush: function() { | |
if (!this.recordPropertyChanges) return this; | |
var changes = this.recordPropertyChanges, | |
storeKeys = changes.storeKeys, | |
hasDataChanges = changes.hasDataChanges, | |
records = changes.records, | |
propertyForStoreKeys = changes.propertyForStoreKeys, | |
recordTypes = SC.Set.create(), | |
rec, recordType, statusOnly, idx, len, storeKey, keys; | |
storeKeys.forEach(function(storeKey) { | |
if (records.contains(storeKey)) { | |
statusOnly = hasDataChanges.contains(storeKey) ? NO : YES; | |
rec = this.records[storeKey]; | |
keys = propertyForStoreKeys ? propertyForStoreKeys[storeKey] : null; | |
// Are we invalidating all keys? If so, don't pass any to | |
// storeDidChangeProperties. | |
if (keys === '*') keys = null; | |
// remove it so we don't trigger this twice | |
records.remove(storeKey); | |
if (rec) rec.storeDidChangeProperties(statusOnly, keys); | |
} | |
recordType = SC.Store.recordTypeFor(storeKey); | |
recordTypes.add(recordType); | |
}, this); | |
if (get(storeKeys, 'length') > 0) this._notifyRecordArrays(storeKeys, recordTypes); | |
storeKeys.clear(); | |
hasDataChanges.clear(); | |
records.clear(); | |
// Provide full reference to overwrite | |
this.recordPropertyChanges.propertyForStoreKeys = {}; | |
return this; | |
}, | |
/** | |
Resets the store content. This will clear all internal data for all | |
records, resetting them to an EMPTY state. You generally do not want | |
to call this method yourself, though you may override it. | |
@returns {SC.Store} receiver | |
*/ | |
reset: function() { | |
// create a new empty data store | |
this.dataHashes = {} ; | |
this.revisions = {} ; | |
this.statuses = {} ; | |
// 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 {SC.Store} nestedStore the child store | |
@param {SC.Set} changes the set of changed store keys | |
@param {Boolean} force | |
@returns {SC.Store} receiver | |
*/ | |
commitChangesFromNestedStore: function(nestedStore, changes, force) { | |
// first, check for optimistic locking problems | |
if (!force) this._verifyLockRevisions(changes, nestedStore.locks); | |
// OK, no locking issues. So let's just copy them changes. | |
// get local reference to values. | |
var len = changes.length, i, storeKey, myDataHashes, myStatuses, | |
myEditables, myRevisions, myParentRecords, myChildRecords, | |
chDataHashes, chStatuses, chRevisions, chParentRecords, chChildRecords; | |
myRevisions = this.revisions ; | |
myDataHashes = this.dataHashes; | |
myStatuses = this.statuses; | |
myEditables = this.editables ; | |
myParentRecords = this.parentRecords ? this.parentRecords : this.parentRecords ={} ; | |
myChildRecords = this.childRecords ? this.childRecords : this.childRecords = {} ; | |
// setup some arrays if needed | |
if (!myEditables) myEditables = this.editables = [] ; | |
chDataHashes = nestedStore.dataHashes; | |
chRevisions = nestedStore.revisions ; | |
chStatuses = nestedStore.statuses; | |
chParentRecords = nestedStore.parentRecords || {}; | |
chChildRecords = nestedStore.childRecords || {}; | |
for(i=0;i<len;i++) { | |
storeKey = changes[i]; | |
// now copy changes | |
myDataHashes[storeKey] = chDataHashes[storeKey]; | |
myStatuses[storeKey] = chStatuses[storeKey]; | |
myRevisions[storeKey] = chRevisions[storeKey]; | |
myParentRecords[storeKey] = chParentRecords[storeKey]; | |
myChildRecords[storeKey] = chChildRecords[storeKey]; | |
myEditables[storeKey] = 0 ; // always make dataHash no longer editable | |
this._notifyRecordPropertyChange(storeKey, NO); | |
} | |
// add any records to the changelog for commit handling | |
var myChangelog = this.changelog, chChangelog = nestedStore.changelog; | |
if (chChangelog) { | |
if (!myChangelog) myChangelog = this.changelog = SC.Set.create(); | |
myChangelog.addEach(chChangelog); | |
} | |
this.changelog = myChangelog; | |
// immediately flush changes to notify records - nested stores will flush | |
// later on. | |
if (!get(this, 'parentStore')) this.flush(); | |
return this ; | |
}, | |
/** @private | |
Verifies that the passed lock revisions match the current revisions | |
in the receiver store. If the lock revisions do not match, then the | |
store is in a conflict and an exception will be raised. | |
@param {Array} changes set of changes we are trying to apply | |
@param {SC.Set} locks the locks to verify | |
@returns {SC.Store} receiver | |
*/ | |
_verifyLockRevisions: function(changes, locks) { | |
var len = changes.length, revs = this.revisions, i, storeKey, lock, rev ; | |
if (locks && revs) { | |
for(i=0;i<len;i++) { | |
storeKey = changes[i]; | |
lock = locks[storeKey] || 1; | |
rev = revs[storeKey] || 1; | |
// if the save revision for the item does not match the current rev | |
// the someone has changed the data hash in this store and we have | |
// a conflict. | |
if (lock < rev) throw SC.Store.CHAIN_CONFLICT_ERROR; | |
} | |
} | |
return this ; | |
}, | |
// .......................................................... | |
// HIGH-LEVEL RECORD API | |
// | |
/** | |
Finds a single record instance with the specified `recordType` and id or | |
an array of records matching some query conditions. | |
Finding a Single Record | |
--- | |
If you pass a single `recordType` and id, this method will return an | |
actual record instance. If the record has not been loaded into the store | |
yet, this method will ask the data source to retrieve it. If no data | |
source indicates that it can retrieve the record, then this method will | |
return `null`. | |
Note that if the record needs to be retrieved from the server, then the | |
record instance returned from this method will not have any data yet. | |
Instead it will have a status of `SC.Record.READY_LOADING`. You can | |
monitor the status property to be notified when the record data is | |
available for you to use it. | |
Find a Collection of Records | |
--- | |
If you pass only a record type or a query object, you can instead find | |
all records matching a specified set of conditions. When you call | |
`find()` in this way, it will create a query if needed and pass it to the | |
data source to fetch the results. | |
If this is the first time you have fetched the query, then the store will | |
automatically ask the data source to fetch any records related to it as | |
well. Otherwise you can refresh the query results at anytime by calling | |
`refresh()` on the returned `RecordArray`. | |
You can detect whether a RecordArray is fetching from the server by | |
checking its status. | |
Examples | |
--- | |
Finding a single record: | |
MyApp.store.find(MyApp.Contact, "23"); // returns MyApp.Contact | |
Finding all records of a particular type: | |
MyApp.store.find(MyApp.Contact); // returns SC.RecordArray of contacts | |
Finding all contacts with first name John: | |
var query = SC.Query.local(MyApp.Contact, "firstName = %@", "John"); | |
MyApp.store.find(query); // returns SC.RecordArray of contacts | |
Finding all contacts using a remote query: | |
var query = SC.Query.remote(MyApp.Contact); | |
MyApp.store.find(query); // returns SC.RecordArray filled by server | |
@param {SC.Record|String} recordType the expected record type | |
@param {String} id the id to load | |
@returns {SC.Record} record instance or null | |
*/ | |
find: function(recordType, id) { | |
// if recordType is passed as string, find object | |
if ('string' === typeof recordType) { | |
recordType = getPath(recordType); | |
} | |
// handle passing a query... | |
if (id === undefined && !(recordType instanceof SC.Record)) { | |
sc_assert('SC.Store#find() accepts only a record type of query', | |
SC.Record.detect(recordType) || recordType instanceof SC.Query); | |
if (!(recordType instanceof SC.Query)) { | |
recordType = SC.Query.local(recordType); | |
} | |
return this._findQuery(recordType, YES, YES); | |
// handle finding a single record | |
} else { | |
return this._findRecord(recordType, id); | |
} | |
}, | |
/** @private | |
DEPRECATED used find() instead. | |
This method will accept a record type or query and return a record array | |
matching the results. This method was commonly used prior to SproutCore | |
1.0. It has been deprecated in favor of a single `find()` method instead. | |
For compatibility, this method will continue to work in SproutCore 1.0 but | |
it will raise a warning. It will be removed in a future version of | |
SproutCore. | |
*/ | |
findAll: function(recordType, conditions, params) { | |
SC.Logger.warn("SC.Store#findAll() will be removed in a future version of SproutCore. Use SC.Store#find() instead"); | |
if (!recordType || !recordType.isQuery) { | |
recordType = SC.Query.local(recordType, conditions, params); | |
} | |
return this._findQuery(recordType, YES, YES); | |
}, | |
_findQuery: function(query, createIfNeeded, refreshIfNew) { | |
// lookup the local RecordArray for this query. | |
var cache = this._scst_recordArraysByQuery, | |
key = SC.guidFor(query), | |
ret, ra ; | |
if (!cache) cache = this._scst_recordArraysByQuery = {}; | |
ret = cache[key]; | |
// if a RecordArray was not found, then create one and also add it to the | |
// list of record arrays to update. | |
if (!ret && createIfNeeded) { | |
cache[key] = ret = SC.RecordArray.create({ store: this, query: query }); | |
ra = get(this, 'recordArrays'); | |
if (!ra) set(this, 'recordArrays', ra = SC.Set.create()); | |
ra.add(ret); | |
if (refreshIfNew) this.refreshQuery(query); | |
} | |
this.flush(); | |
return ret ; | |
}, | |
_findRecord: function(recordType, id) { | |
var storeKey ; | |
// 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 (recordType && (recordType instanceof SC.Record)) { | |
storeKey = get(recordType, 'storeKey'); | |
// otherwise, lookup the storeKey for the passed id. look in subclasses | |
// as well. | |
} else storeKey = id ? recordType.storeKeyFor(id) : null; | |
if (storeKey && (this.readStatus(storeKey) === SC.Record.EMPTY)) { | |
storeKey = this.retrieveRecord(recordType, id); | |
} | |
// now we have the storeKey, materialize the record and return it. | |
return storeKey ? this.materializeRecord(storeKey) : null ; | |
}, | |
// .......................................................... | |
// RECORD ARRAY OPERATIONS | |
// | |
/** | |
Called by the record array just before it is destroyed. This will | |
de-register it from receiving future notifications. | |
You should never call this method yourself. Instead call `destroy()` on | |
the `RecordArray` directly. | |
@param {SC.RecordArray} recordArray the record array | |
@returns {SC.Store} receiver | |
*/ | |
recordArrayWillDestroy: function(recordArray) { | |
var cache = this._scst_recordArraysByQuery, | |
set = get(this, 'recordArrays'); | |
if (cache) delete cache[SC.guidFor(get(recordArray, 'query'))]; | |
if (set) set.remove(recordArray); | |
return this ; | |
}, | |
/** | |
Called by the record array whenever it needs the data source to refresh | |
its contents. Nested stores will actually just pass this along to the | |
parent store. The parent store will call `fetch()` on the data source. | |
You should never call this method yourself. Instead call `refresh()` on | |
the `RecordArray` directly. | |
@param {SC.Query} query the record array query to refresh | |
@returns {SC.Store} receiver | |
*/ | |
refreshQuery: function(query) { | |
if (!query) throw new Error("refreshQuery() requires a query"); | |
var cache = this._scst_recordArraysByQuery, | |
recArray = cache ? cache[SC.guidFor(query)] : null, | |
source = this._getDataSource(); | |
if (source && source.fetch) { | |
if (recArray) recArray.storeWillFetchQuery(query); | |
source.fetch.call(source, this, query); | |
} | |
return this ; | |
}, | |
/** @private | |
Will ask all record arrays that have been returned from `findAll` | |
with an `SC.Query` to check their arrays with the new `storeKey`s | |
@param {SC.IndexSet} storeKeys set of storeKeys that changed | |
@param {SC.Set} recordTypes | |
@returns {SC.Store} receiver | |
*/ | |
_notifyRecordArrays: function(storeKeys, recordTypes) { | |
var recordArrays = get(this, 'recordArrays'); | |
if (!recordArrays) return this; | |
recordArrays.forEach(function(recArray) { | |
if (recArray) recArray.storeDidChangeStoreKeys(storeKeys, recordTypes); | |
}, this); | |
return this ; | |
}, | |
// .......................................................... | |
// LOW-LEVEL HELPERS | |
// | |
/** | |
Array of all records currently in the store with the specified | |
type. This method only reflects the actual records loaded into memory and | |
therefore is not usually needed at runtime. However you will often use | |
this method for testing. | |
@param {SC.Record} recordType the record type | |
@returns {SC.Array} array instance - usually SC.RecordArray | |
*/ | |
recordsFor: function(recordType) { | |
var storeKeys = [], | |
storeKeysById = recordType.storeKeysById(), | |
id, storeKey, ret; | |
// collect all non-empty store keys | |
for(id in storeKeysById) { | |
storeKey = storeKeysById[id]; // get the storeKey | |
if (this.readStatus(storeKey) !== SC.RECORD_EMPTY) { | |
storeKeys.push(storeKey); | |
} | |
} | |
if (storeKeys.length>0) { | |
ret = SC.RecordArray.create({ store: this, storeKeys: storeKeys }); | |
} else ret = storeKeys; // empty array | |
return ret ; | |
}, | |
_TMP_REC_ATTRS: {}, | |
/** | |
Given a `storeKey`, return a materialized record. You will not usually | |
call this method yourself. Instead it will used by other methods when | |
you find records by id or perform other searches. | |
If a `recordType` has been mapped to the storeKey, then a record instance | |
will be returned even if the data hash has not been requested yet. | |
Each Store instance returns unique record instances for each storeKey. | |
@param {Number} storeKey The storeKey for the dataHash. | |
@returns {SC.Record} Returns a record instance. | |
*/ | |
materializeRecord: function(storeKey) { | |
var records = this.records, ret, recordType, attrs; | |
// look up in cached records | |
if (!records) records = this.records = {}; // load cached records | |
ret = records[storeKey]; | |
if (ret) return ret; | |
// not found -- OK, create one then. | |
recordType = SC.Store.recordTypeFor(storeKey); | |
if (!recordType) return null; // not recordType registered, nothing to do | |
attrs = this._TMP_REC_ATTRS ; | |
attrs.storeKey = storeKey ; | |
attrs.store = this ; | |
ret = records[storeKey] = recordType.create(attrs); | |
return ret ; | |
}, | |
// .......................................................... | |
// CORE RECORDS API | |
// | |
// The methods in this section can be used to manipulate records without | |
// actually creating record instances. | |
/** | |
Creates a new record instance with the passed `recordType` and `dataHash`. | |
You can also optionally specify an id or else it will be pulled from the | |
data hash. | |
Note that the record will not yet be saved back to the server. To save | |
a record to the server, call `commitChanges()` on the store. | |
@param {SC.Record} recordType the record class to use on creation | |
@param {Hash} dataHash the JSON attributes to assign to the hash. | |
@param {String} id (optional) id to assign to record | |
@returns {SC.Record} Returns the created record | |
*/ | |
createRecord: function(recordType, dataHash, id) { | |
var primaryKey, storeKey, status, K = SC.Record, changelog, 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 = SC.RecordAttribute.attrFor(get(recordType, 'proto'), primaryKey); | |
defaultVal = attr && get(attr, 'defaultValue'); | |
if(!id && SC.typeOf(defaultVal)==='function') { | |
id = dataHash[primaryKey] = defaultVal(); | |
} | |
} | |
// Next get the storeKey - base on id if available | |
storeKey = id ? recordType.storeKeyFor(id) : SC.Store.generateStoreKey(); | |
// now, check the state and do the right thing. | |
status = this.readStatus(storeKey); | |
// check state | |
// any busy or ready state or destroyed dirty state is not allowed | |
if ((status & K.BUSY) || | |
(status & K.READY) || | |
(status === K.DESTROYED_DIRTY)) { | |
throw id ? K.RECORD_EXISTS_ERROR : K.BAD_STATE_ERROR; | |
// allow error or destroyed state only with id | |
} else if (!id && (status===SC.DESTROYED_CLEAN || status===SC.StoreError)) { | |
throw K.BAD_STATE_ERROR; | |
} | |
// add dataHash and setup initial status -- also save recordType | |
this.writeDataHash(storeKey, (dataHash ? dataHash : {}), K.READY_NEW); | |
SC.Store.replaceRecordTypeFor(storeKey, recordType); | |
this.dataHashDidChange(storeKey); | |
// Record is now in a committable state -- add storeKey to changelog | |
changelog = this.changelog; | |
if (!changelog) changelog = SC.Set.create(); | |
changelog.add(storeKey); | |
this.changelog = changelog; | |
// if commit records is enabled | |
if(get(this, 'commitRecordsAutomatically')){ | |
SC.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 {SC.Record|Array} recordTypes class or array of classes | |
@param {Array} dataHashes array of data hashes | |
@param {Array} ids (optional) ids to assign to records | |
@returns {Array} array of materialized record instances. | |
*/ | |
createRecords: function(recordTypes, dataHashes, ids) { | |
var ret = [], recordType, id, isArray, len = dataHashes.length, idx ; | |
isArray = SC.typeOf(recordTypes) === 'array'; | |
if (!isArray) recordType = recordTypes; | |
for(idx=0;idx<len;idx++) { | |
if (isArray) recordType = recordTypes[idx] || SC.Record; | |
id = ids ? ids[idx] : undefined ; | |
ret.push(this.createRecord(recordType, dataHashes[idx], id)); | |
} | |
return ret ; | |
}, | |
/** | |
Unloads a record, removing the data hash from the store. If you try to | |
unload a record that is already destroyed then this method will have no effect. | |
If you unload a record that does not exist or an error then an exception | |
will be raised. | |
@param {SC.Record} recordType the recordType | |
@param {String} id the record id | |
@param {Number} storeKey (optional) if passed, ignores recordType and id | |
@returns {SC.Store} receiver | |
*/ | |
unloadRecord: function(recordType, id, storeKey, newStatus) { | |
if (storeKey === undefined) storeKey = recordType.storeKeyFor(id); | |
var status = this.readStatus(storeKey), K = SC.Record; | |
newStatus = newStatus || K.EMPTY; | |
// handle status - ignore if destroying or destroyed | |
if ((status === K.BUSY_DESTROYING) || (status & K.DESTROYED)) { | |
return this; // nothing to do | |
// error out if empty | |
} else if (status & K.BUSY) { | |
throw K.BUSY_ERROR ; | |
// otherwise, destroy in dirty state | |
} else status = newStatus ; | |
// remove the data hash, set new status | |
this.removeDataHash(storeKey, status); | |
this.dataHashDidChange(storeKey); | |
// Handle all the child Records | |
var that = this; | |
this._propagateToChildren(storeKey, function(storeKey){ | |
that.unloadRecord(null, null, storeKey, newStatus); | |
}); | |
return this ; | |
}, | |
/** | |
Unloads a group of records. If you have a set of record ids, unloading | |
them this way can be faster than retrieving each record and unloading | |
it individually. | |
You can pass either a single `recordType` or an array of `recordType`s. If | |
you pass a single `recordType`, then the record type will be used for each | |
record. If you pass an array, then each id must have a matching record | |
type in the array. | |
You can optionally pass an array of `storeKey`s instead of the `recordType` | |
and ids. In this case the first two parameters will be ignored. This | |
is usually only used by low-level internal methods. You will not usually | |
unload records this way. | |
@param {SC.Record|Array} recordTypes class or array of classes | |
@param {Array} ids (optional) ids to unload | |
@param {Array} storeKeys (optional) store keys to unload | |
@returns {SC.Store} receiver | |
*/ | |
unloadRecords: function(recordTypes, ids, storeKeys, newStatus) { | |
var len, isArray, idx, id, recordType, storeKey; | |
if (storeKeys === undefined) { | |
isArray = SC.typeOf(recordTypes) === 'array'; | |
if (!isArray) recordType = recordTypes; | |
if (ids === undefined) { | |
len = isArray ? recordTypes.length : 1; | |
for (idx = 0; idx < len; idx++) { | |
if (isArray) recordType = recordTypes[idx]; | |
storeKeys = this.storeKeysFor(recordType); | |
this.unloadRecords(undefined, undefined, storeKeys, newStatus); | |
} | |
} else { | |
len = ids.length; | |
for (idx = 0; idx < len; idx++) { | |
if (isArray) recordType = recordTypes[idx] || SC.Record; | |
id = ids ? ids[idx] : undefined; | |
this.unloadRecord(recordType, id, undefined, newStatus); | |
} | |
} | |
} else { | |
len = storeKeys.length; | |
for (idx = 0; idx < len; idx++) { | |
storeKey = storeKeys ? storeKeys[idx] : undefined; | |
this.unloadRecord(undefined, undefined, storeKey, newStatus); | |
} | |
} | |
return this; | |
}, | |
/** | |
Destroys a record, removing the data hash from the store and adding the | |
record to the destroyed changelog. If you try to destroy a record that is | |
already destroyed then this method will have no effect. If you destroy a | |
record that does not exist or an error then an exception will be raised. | |
@param {SC.Record} recordType the recordType | |
@param {String} id the record id | |
@param {Number} storeKey (optional) if passed, ignores recordType and id | |
@returns {SC.Store} receiver | |
*/ | |
destroyRecord: function(recordType, id, storeKey) { | |
if (storeKey === undefined) storeKey = recordType.storeKeyFor(id); | |
var status = this.readStatus(storeKey), changelog, K = SC.Record; | |
// handle status - ignore if destroying or destroyed | |
if ((status === K.BUSY_DESTROYING) || (status & K.DESTROYED)) { | |
return this; // nothing to do | |
// error out if empty | |
} else if (status === K.EMPTY) { | |
throw K.NOT_FOUND_ERROR ; | |
// error out if busy | |
} else if (status & K.BUSY) { | |
throw K.BUSY_ERROR ; | |
// if new status, destroy but leave in clean state | |
} else if (status === K.READY_NEW) { | |
status = K.DESTROYED_CLEAN ; | |
// otherwise, destroy in dirty state | |
} else status = K.DESTROYED_DIRTY ; | |
// remove the data hash, set new status | |
this.writeStatus(storeKey, status); | |
this.dataHashDidChange(storeKey); | |
// add/remove change log | |
changelog = this.changelog; | |
if (!changelog) changelog = this.changelog = SC.Set.create(); | |
((status & K.DIRTY) ? changelog.add(storeKey) : changelog.remove(storeKey)); | |
this.changelog=changelog; | |
// if commit records is enabled | |
if(get(this, 'commitRecordsAutomatically')){ | |
SC.run.schedule('actions', this, this.commitRecords); | |
} | |
var that = this; | |
this._propagateToChildren(storeKey, function(storeKey){ | |
that.destroyRecord(null, null, storeKey); | |
}); | |
return this ; | |
}, | |
/** | |
Destroys a group of records. If you have a set of record ids, destroying | |
them this way can be faster than retrieving each record and destroying | |
it individually. | |
You can pass either a single `recordType` or an array of `recordType`s. If | |
you pass a single `recordType`, then the record type will be used for each | |
record. If you pass an array, then each id must have a matching record | |
type in the array. | |
You can optionally pass an array of `storeKey`s instead of the `recordType` | |
and ids. In this case the first two parameters will be ignored. This | |
is usually only used by low-level internal methods. You will not usually | |
destroy records this way. | |
@param {SC.Record|Array} recordTypes class or array of classes | |
@param {Array} ids ids to destroy | |
@param {Array} storeKeys (optional) store keys to destroy | |
@returns {SC.Store} receiver | |
*/ | |
destroyRecords: function(recordTypes, ids, storeKeys) { | |
var len, isArray, idx, id, recordType, storeKey; | |
if(storeKeys===undefined){ | |
len = ids.length; | |
isArray = SC.typeOf(recordTypes) === 'array'; | |
if (!isArray) recordType = recordTypes; | |
for(idx=0;idx<len;idx++) { | |
if (isArray) recordType = recordTypes[idx] || SC.Record; | |
id = ids ? ids[idx] : undefined ; | |
this.destroyRecord(recordType, id, undefined); | |
} | |
}else{ | |
len = storeKeys.length; | |
for(idx=0;idx<len;idx++) { | |
storeKey = storeKeys ? storeKeys[idx] : undefined ; | |
this.destroyRecord(undefined, undefined, storeKey); | |
} | |
} | |
return this ; | |
}, | |
/** | |
register a Child Record to the parent | |
*/ | |
registerChildToParent: function(parentStoreKey, childStoreKey, path){ | |
var prs, crs, oldPk, oldChildren, pkRef; | |
// Check the child to see if it has a parent | |
crs = this.childRecords || {}; | |
prs = this.parentRecords || {}; | |
// first rid of the old parent | |
oldPk = crs[childStoreKey]; | |
if (oldPk){ | |
oldChildren = prs[oldPk]; | |
delete oldChildren[childStoreKey]; | |
// this.recordDidChange(null, null, oldPk, key); | |
} | |
pkRef = prs[parentStoreKey] || {}; | |
pkRef[childStoreKey] = path || YES; | |
prs[parentStoreKey] = pkRef; | |
crs[childStoreKey] = parentStoreKey; | |
// sync the status of the child | |
this.writeStatus(childStoreKey, this.statuses[parentStoreKey]); | |
this.childRecords = crs; | |
this.parentRecords = prs; | |
}, | |
/** | |
materialize the parent when passing in a store key for the child | |
*/ | |
materializeParentRecord: function(childStoreKey){ | |
var pk, crs; | |
if (none(childStoreKey)) return null; | |
crs = this.childRecords; | |
pk = crs ? this.childRecords[childStoreKey] : null ; | |
if (none(pk)) return null; | |
return this.materializeRecord(pk); | |
}, | |
/** | |
function for retrieving a parent record key | |
@param {Number} storeKey The store key of the parent | |
*/ | |
parentStoreKeyExists: function(storeKey){ | |
if (none(storeKey)) return ; | |
var crs = this.childRecords || {}; | |
return crs[storeKey]; | |
}, | |
/** | |
function that propagates a function call to all children | |
*/ | |
_propagateToChildren: function(storeKey, func){ | |
// Handle all the child Records | |
if ( none(this.parentRecords) ) return; | |
var children = this.parentRecords[storeKey] || {}; | |
if (none(func)) return; | |
for (var key in children) { | |
if (children.hasOwnProperty(key)) func(key); | |
} | |
}, | |
/** | |
Notes that the data for the given record id has changed. The record will | |
be committed to the server the next time you commit the root store. Only | |
call this method on a record in a READY state of some type. | |
@param {SC.Record} recordType the recordType | |
@param {String} id the record id | |
@param {Number} storeKey (optional) if passed, ignores recordType and id | |
@param {String} key that changed (optional) | |
@param {Boolean} if the change is to statusOnly (optional) | |
@returns {SC.Store} receiver | |
*/ | |
recordDidChange: function(recordType, id, storeKey, key, statusOnly) { | |
if (storeKey === undefined) storeKey = recordType.storeKeyFor(id); | |
var status = this.readStatus(storeKey), changelog, K = SC.Record; | |
// BUSY_LOADING, BUSY_CREATING, BUSY_COMMITTING, BUSY_REFRESH_CLEAN | |
// BUSY_REFRESH_DIRTY, BUSY_DESTROYING | |
if (status & K.BUSY) { | |
throw K.BUSY_ERROR ; | |
// if record is not in ready state, then it is not found. | |
// ERROR, EMPTY, DESTROYED_CLEAN, DESTROYED_DIRTY | |
} else if (!(status & K.READY)) { | |
throw K.NOT_FOUND_ERROR ; | |
// otherwise, make new status READY_DIRTY unless new. | |
// K.READY_CLEAN, K.READY_DIRTY, ignore K.READY_NEW | |
} else { | |
if (status != K.READY_NEW) this.writeStatus(storeKey, K.READY_DIRTY); | |
} | |
// record data hash change | |
this.dataHashDidChange(storeKey, null, statusOnly, key); | |
// record in changelog | |
changelog = this.changelog ; | |
if (!changelog) changelog = this.changelog = SC.Set.create() ; | |
changelog.add(storeKey); | |
this.changelog = changelog; | |
// if commit records is enabled | |
if(get(this, 'commitRecordsAutomatically')){ | |
SC.run.schedule('actions', this, this.commitRecords); | |
} | |
return this ; | |
}, | |
/** | |
Mark a group of records as dirty. The records will be committed to the | |
server the next time you commit changes on the root store. If you have a | |
set of record ids, marking them dirty this way can be faster than | |
retrieving each record and destroying it individually. | |
You can pass either a single `recordType` or an array of `recordType`s. If | |
you pass a single `recordType`, then the record type will be used for each | |
record. If you pass an array, then each id must have a matching record | |
type in the array. | |
You can optionally pass an array of `storeKey`s instead of the `recordType` | |
and ids. In this case the first two parameters will be ignored. This | |
is usually only used by low-level internal methods. | |
@param {SC.Record|Array} recordTypes class or array of classes | |
@param {Array} ids ids to destroy | |
@param {Array} storeKeys (optional) store keys to destroy | |
@returns {SC.Store} receiver | |
*/ | |
recordsDidChange: function(recordTypes, ids, storeKeys) { | |
var len, isArray, idx, id, recordType, storeKey; | |
if(storeKeys===undefined){ | |
len = ids.length; | |
isArray = SC.typeOf(recordTypes) === 'array'; | |
if (!isArray) recordType = recordTypes; | |
for(idx=0;idx<len;idx++) { | |
if (isArray) recordType = recordTypes[idx] || SC.Record; | |
id = ids ? ids[idx] : undefined ; | |
storeKey = storeKeys ? storeKeys[idx] : undefined ; | |
this.recordDidChange(recordType, id, storeKey); | |
} | |
}else{ | |
len = storeKeys.length; | |
for(idx=0;idx<len;idx++) { | |
storeKey = storeKeys ? storeKeys[idx] : undefined ; | |
this.recordDidChange(undefined, undefined, storeKey); | |
} | |
} | |
return this ; | |
}, | |
/** | |
Retrieves a set of records from the server. If the records has | |
already been loaded in the store, then this method will simply return. | |
Otherwise if your store has a `dataSource`, this will call the | |
`dataSource` to retrieve the record. Generally you will not need to | |
call this method yourself. Instead you can just use `find()`. | |
This will not actually create a record instance but it will initiate a | |
load of the record from the server. You can subsequently get a record | |
instance itself using `materializeRecord()`. | |
@param {SC.Record|Array} recordTypes class or array of classes | |
@param {Array} ids ids to retrieve | |
@param {Array} storeKeys (optional) store keys to retrieve | |
@param {Boolean} isRefresh | |
@param {Function|Array} callback function or array of functions | |
@returns {Array} storeKeys to be retrieved | |
*/ | |
retrieveRecords: function(recordTypes, ids, storeKeys, isRefresh, callbacks) { | |
var source = this._getDataSource(), | |
isArray = SC.typeOf(recordTypes) === 'array', | |
hasCallbackArray = SC.typeOf(callbacks) === 'array', | |
len = (!storeKeys) ? ids.length : storeKeys.length, | |
ret = [], | |
rev = SC.Store.generateStoreKey(), | |
K = SC.Record, | |
recordType, idx, storeKey, status, ok, callback; | |
if (!isArray) recordType = recordTypes; | |
// if no storeKeys were passed, map recordTypes + ids | |
for(idx=0;idx<len;idx++) { | |
// collect store key | |
if (storeKeys) { | |
storeKey = storeKeys[idx]; | |
} else { | |
if (isArray) recordType = recordTypes[idx]; | |
storeKey = recordType.storeKeyFor(ids[idx]); | |
} | |
//collect the callback | |
callback = hasCallbackArray ? callbacks[idx] : callbacks; | |
// collect status and process | |
status = this.readStatus(storeKey); | |
// K.EMPTY, K.ERROR, K.DESTROYED_CLEAN - initial retrieval | |
if ((status == K.EMPTY) || (status == K.ERROR) || (status == K.DESTROYED_CLEAN)) { | |
this.writeStatus(storeKey, K.BUSY_LOADING); | |
this.dataHashDidChange(storeKey, rev, YES); | |
ret.push(storeKey); | |
this._setCallbackForStoreKey(storeKey, callback, hasCallbackArray, storeKeys); | |
// otherwise, ignore record unless isRefresh is YES. | |
} else if (isRefresh) { | |
// K.READY_CLEAN, K.READY_DIRTY, ignore K.READY_NEW | |
if (status & K.READY) { | |
this.writeStatus(storeKey, K.BUSY_REFRESH | (status & 0x03)) ; | |
this.dataHashDidChange(storeKey, rev, YES); | |
ret.push(storeKey); | |
this._setCallbackForStoreKey(storeKey, callback, hasCallbackArray, storeKeys); | |
// K.BUSY_DESTROYING, K.BUSY_COMMITTING, K.BUSY_CREATING | |
} else if ((status == K.BUSY_DESTROYING) || (status == K.BUSY_CREATING) || (status == K.BUSY_COMMITTING)) { | |
throw K.BUSY_ERROR ; | |
// K.DESTROY_DIRTY, bad state... | |
} else if (status == K.DESTROYED_DIRTY) { | |
throw K.BAD_STATE_ERROR ; | |
// ignore K.BUSY_LOADING, K.BUSY_REFRESH_CLEAN, K.BUSY_REFRESH_DIRTY | |
} | |
} | |
} | |
// now retrieve storekeys from dataSource. if there is no dataSource, | |
// then act as if we couldn't retrieve. | |
ok = NO; | |
if (source) ok = source.retrieveRecords.call(source, this, ret, ids); | |
// if the data source could not retrieve or if there is no source, then | |
// simulate the data source calling dataSourceDidError on those we are | |
// loading for the first time or dataSourceDidComplete on refreshes. | |
if (!ok) { | |
len = ret.length; | |
rev = SC.Store.generateStoreKey(); | |
for(idx=0;idx<len;idx++) { | |
storeKey = ret[idx]; | |
status = this.readStatus(storeKey); | |
if (status === K.BUSY_LOADING) { | |
this.writeStatus(storeKey, K.ERROR); | |
this.dataHashDidChange(storeKey, rev, YES); | |
} else if (status & K.BUSY_REFRESH) { | |
this.writeStatus(storeKey, K.READY | (status & 0x03)); | |
this.dataHashDidChange(storeKey, rev, YES); | |
} | |
} | |
ret.length = 0 ; // truncate to indicate that none could refresh | |
} | |
return ret ; | |
}, | |
_TMP_RETRIEVE_ARRAY: [], | |
_callback_queue: {}, | |
/** | |
@private | |
stores the callbacks for the storeKeys that are inflight | |
**/ | |
_setCallbackForStoreKey: function(storeKey, callback, hasCallbackArray, storeKeys){ | |
var queue = this._callback_queue; | |
if(hasCallbackArray) queue[storeKey] = {callback: callback, otherKeys: storeKeys}; | |
else queue[storeKey] = callback; | |
}, | |
/** | |
@private | |
retreives and calls callback for `storkey` if exists | |
also handles if a single callback is need for one key | |
**/ | |
_retreiveCallbackForStoreKey: function(storeKey){ | |
var queue = this._callback_queue, | |
callback = queue[storeKey], | |
allFinished, keys; | |
if(callback){ | |
if(SC.typeOf(callback) === 'function'){ | |
callback.call(); //args? | |
delete queue[storeKey]; //cleanup | |
} | |
else if(SC.typeOf(callback) == 'object'){ | |
callback.completed = YES; | |
keys = callback.storeKeys; | |
keys.forEach(function(key){ | |
if(!queue[key].completed) allFinished = YES; | |
}); | |
if(allFinished){ | |
callback.callback.call(); // args? | |
//cleanup | |
keys.forEach(function(key){ | |
delete queue[key]; | |
}); | |
} | |
} | |
} | |
}, | |
/* | |
@private | |
*/ | |
_cancelCallback: function(storeKey){ | |
var queue = this._callback_queue; | |
if(queue[storeKey]){ | |
delete queue[storeKey]; | |
} | |
}, | |
/** | |
Retrieves a record from the server. If the record has already been loaded | |
in the store, then this method will simply return. Otherwise if your | |
store has a `dataSource`, this will call the `dataSource` to retrieve the | |
record. Generally you will not need to call this method yourself. | |
Instead you can just use `find()`. | |
This will not actually create a record instance but it will initiate a | |
load of the record from the server. You can subsequently get a record | |
instance itself using `materializeRecord()`. | |
@param {SC.Record} recordType class | |
@param {String} id id to retrieve | |
@param {Number} storeKey (optional) store key | |
@param {Boolean} isRefresh | |
@param {Function} callback (optional) | |
@returns {Number} storeKey that was retrieved | |
*/ | |
retrieveRecord: function(recordType, id, storeKey, isRefresh, callback) { | |
var array = this._TMP_RETRIEVE_ARRAY, | |
ret; | |
if (storeKey) { | |
array[0] = storeKey; | |
storeKey = array; | |
id = null ; | |
} else { | |
array[0] = id; | |
id = array; | |
} | |
ret = this.retrieveRecords(recordType, id, storeKey, isRefresh, callback); | |
array.length = 0 ; | |
return ret[0]; | |
}, | |
/** | |
Refreshes a record from the server. If the record has already been loaded | |
in the store, then this method will request a refresh from the | |
`dataSource`. Otherwise it will attempt to retrieve the record. | |
@param {String} id to id of the record to load | |
@param {SC.Record} recordType the expected record type | |
@param {Number} storeKey (optional) optional store key | |
@param {Function} callback (optional) when refresh complets | |
@returns {Boolean} YES if the retrieval was a success. | |
*/ | |
refreshRecord: function(recordType, id, storeKey, callback) { | |
return !!this.retrieveRecord(recordType, id, storeKey, YES, callback); | |
}, | |
/** | |
Refreshes a set of records from the server. If the records has already been loaded | |
in the store, then this method will request a refresh from the | |
`dataSource`. Otherwise it will attempt to retrieve them. | |
@param {SC.Record|Array} recordTypes class or array of classes | |
@param {Array} ids ids to destroy | |
@param {Array} storeKeys (optional) store keys to destroy | |
@param {Function} callback (optional) when refresh complets | |
@returns {Boolean} YES if the retrieval was a success. | |
*/ | |
refreshRecords: function(recordTypes, ids, storeKeys, callback) { | |
var ret = this.retrieveRecords(recordTypes, ids, storeKeys, YES, callback); | |
return ret && ret.length>0; | |
}, | |
/** | |
Commits the passed store keys or ids. If no `storeKey`s are given, | |
it will commit any records in the changelog. | |
Based on the current state of the record, this will ask the data | |
source to perform the appropriate actions | |
on the store keys. | |
@param {Array} recordTypes the expected record types (SC.Record) | |
@param {Array} ids to commit | |
@param {SC.Set} storeKeys to commit | |
@param {Hash} params optional additional parameters to pass along to the | |
data source | |
@param {Function|Array} callback function or array of callbacks | |
@returns {Boolean} if the action was succesful. | |
*/ | |
commitRecords: function(recordTypes, ids, storeKeys, params, callbacks) { | |
var source = this._getDataSource(), | |
isArray = SC.typeOf(recordTypes) === 'array', | |
hasCallbackArray = SC.typeOf(callbacks) === 'array', | |
retCreate= [], retUpdate= [], retDestroy = [], | |
rev = SC.Store.generateStoreKey(), | |
K = SC.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;idx<len;idx++) { | |
// collect store key | |
if (storeKeys) { | |
storeKey = storeKeys[idx]; | |
} else { | |
if (isArray) recordType = recordTypes[idx] || SC.Record; | |
else recordType = recordTypes; | |
storeKey = recordType.storeKeyFor(ids[idx]); | |
} | |
//collect the callback | |
callback = hasCallbackArray ? callbacks[idx] : callbacks; | |
// collect status and process | |
status = this.readStatus(storeKey); | |
if ((status == K.EMPTY) || (status == K.ERROR)) { | |
throw K.NOT_FOUND_ERROR ; | |
} | |
else { | |
if(status==K.READY_NEW) { | |
this.writeStatus(storeKey, K.BUSY_CREATING); | |
this.dataHashDidChange(storeKey, rev, YES); | |
retCreate.push(storeKey); | |
this._setCallbackForStoreKey(storeKey, callback, hasCallbackArray, storeKeys); | |
} else if (status==K.READY_DIRTY) { | |
this.writeStatus(storeKey, K.BUSY_COMMITTING); | |
this.dataHashDidChange(storeKey, rev, YES); | |
retUpdate.push(storeKey); | |
this._setCallbackForStoreKey(storeKey, callback, hasCallbackArray, storeKeys); | |
} else if (status==K.DESTROYED_DIRTY) { | |
this.writeStatus(storeKey, K.BUSY_DESTROYING); | |
this.dataHashDidChange(storeKey, rev, YES); | |
retDestroy.push(storeKey); | |
this._setCallbackForStoreKey(storeKey, callback, hasCallbackArray, storeKeys); | |
} else if (status==K.DESTROYED_CLEAN) { | |
this.dataHashDidChange(storeKey, rev, YES); | |
} | |
// ignore K.READY_CLEAN, K.BUSY_LOADING, K.BUSY_CREATING, K.BUSY_COMMITTING, | |
// K.BUSY_REFRESH_CLEAN, K_BUSY_REFRESH_DIRTY, KBUSY_DESTROYING | |
} | |
} | |
// now commit storekeys to dataSource | |
if (source && (len>0 || 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 {SC.Record} recordType the expected record type | |
@param {String} id the id of the record to commit | |
@param {Number} storeKey the storeKey of the record to commit | |
@param {Hash} params optional 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 {SC.Record|Array} recordTypes class or array of classes | |
@param {Array} ids ids to destroy | |
@param {Array} storeKeys (optional) store keys to destroy | |
@returns {SC.Store} the store. | |
*/ | |
cancelRecords: function(recordTypes, ids, storeKeys) { | |
var source = this._getDataSource(), | |
isArray = SC.typeOf(recordTypes) === 'array', | |
K = SC.Record, | |
ret = [], | |
status, len, idx, id, recordType, storeKey; | |
len = (storeKeys === undefined) ? ids.length : storeKeys.length; | |
for(idx=0;idx<len;idx++) { | |
if (isArray) recordType = recordTypes[idx] || SC.Record; | |
else recordType = recordTypes || SC.Record; | |
id = ids ? ids[idx] : undefined ; | |
if(storeKeys===undefined){ | |
storeKey = recordType.storeKeyFor(id); | |
}else{ | |
storeKey = storeKeys ? storeKeys[idx] : undefined ; | |
} | |
if(storeKey) { | |
status = this.readStatus(storeKey); | |
if ((status == K.EMPTY) || (status == K.ERROR)) { | |
throw K.NOT_FOUND_ERROR ; | |
} | |
ret.push(storeKey); | |
this._cancelCallback(storeKey); | |
} | |
} | |
if (source) source.cancel.call(source, this, ret); | |
return this ; | |
}, | |
/** | |
Cancels an inflight request for the passed record. Depending on the | |
server implementation, this could cancel an entire request, causing | |
other records to also transition their current state. | |
@param {SC.Record|Array} recordTypes class or array of classes | |
@param {Array} ids ids to destroy | |
@param {Array} storeKeys (optional) store keys to destroy | |
@returns {SC.Store} the store. | |
*/ | |
cancelRecord: function(recordType, id, storeKey) { | |
var array = this._TMP_RETRIEVE_ARRAY, | |
ret ; | |
if (storeKey !== undefined) { | |
array[0] = storeKey; | |
storeKey = array; | |
id = null ; | |
} else { | |
array[0] = id; | |
id = array; | |
} | |
ret = this.cancelRecords(recordType, id, storeKey); | |
array.length = 0 ; | |
return this; | |
}, | |
/** | |
Convenience method can be called by the store or other parts of your | |
application to load a record into the store. This method will take a | |
recordType and a data hashes and either add or update the | |
record in the store. | |
The loaded records will be in an `SC.Record.READY_CLEAN` state, indicating | |
they were loaded from the data source and do not need to be committed | |
back before changing. | |
This method will check the state of the storeKey and call either | |
`pushRetrieve()` or `dataSourceDidComplete()`. The standard state constraints | |
for these methods apply here. | |
The return value will be the `storeKey` used for the push. This is often | |
convenient to pass into `loadQuery()`, if you are fetching a remote query. | |
If you are upgrading from a pre SproutCore 1.0 application, this method | |
is the closest to the old `updateRecord()`. | |
@param {SC.Record} recordType the record type | |
@param {Array} dataHash to update | |
@param {Array} id optional. if not passed lookup on the hash | |
@returns {String} store keys assigned to these id | |
*/ | |
loadRecord: function(recordType, dataHash, id) { | |
var K = SC.Record, | |
ret, primaryKey, storeKey; | |
// save lookup info | |
recordType = recordType || SC.Record; | |
primaryKey = get(recordType, 'proto').primaryKey; | |
// push each record | |
id = id || dataHash[primaryKey]; | |
ret = storeKey = recordType.storeKeyFor(id); // needed to cache | |
if (this.readStatus(storeKey) & K.BUSY) { | |
this.dataSourceDidComplete(storeKey, dataHash, id); | |
} else this.pushRetrieve(recordType, id, dataHash, storeKey); | |
// return storeKey | |
return ret ; | |
}, | |
/** | |
Convenience method can be called by the store or other parts of your | |
application to load records into the store. This method will take a | |
recordType and an array of data hashes and either add or update the | |
record in the store. | |
The loaded records will be in an `SC.Record.READY_CLEAN` state, indicating | |
they were loaded from the data source and do not need to be committed | |
back before changing. | |
This method will check the state of each storeKey and call either | |
`pushRetrieve()` or `dataSourceDidComplete()`. The standard state | |
constraints for these methods apply here. | |
The return value will be the storeKeys used for each push. This is often | |
convenient to pass into `loadQuery()`, if you are fetching a remote query. | |
If you are upgrading from a pre SproutCore 1.0 application, this method | |
is the closest to the old `updateRecords()`. | |
@param {SC.Record} recordTypes the record type or array of record types | |
@param {Array} dataHashes array of data hashes to update | |
@param {Array} ids optional array of ids. if not passed lookup on hashes | |
@returns {Array} store keys assigned to these ids | |
*/ | |
loadRecords: function(recordTypes, dataHashes, ids) { | |
var isArray = SC.typeOf(recordTypes) === 'array', | |
len = get(dataHashes, 'length'), | |
ret = [], | |
K = SC.Record, | |
recordType, id, primaryKey, idx, dataHash, storeKey; | |
// save lookup info | |
if (!isArray) { | |
recordType = recordTypes || SC.Record; | |
primaryKey = get(recordType, 'proto').primaryKey ; | |
} | |
// push each record | |
for(idx=0;idx<len;idx++) { | |
dataHash = dataHashes.objectAt(idx); | |
if (isArray) { | |
recordType = recordTypes.objectAt(idx) || SC.Record; | |
primaryKey = get(recordType, 'proto').primaryKey ; | |
} | |
id = (ids) ? ids.objectAt(idx) : dataHash[primaryKey]; | |
ret[idx] = this.loadRecord(recordType, dataHash, id); | |
} | |
// return storeKeys | |
return ret ; | |
}, | |
/** | |
Returns the `SC.StoreError` object associated with a specific record. | |
@param {Number} storeKey The store key of the record. | |
@returns {SC.StoreError} SC.StoreError or undefined if no error associated with the record. | |
*/ | |
readError: function(storeKey) { | |
var errors = this.recordErrors ; | |
return errors ? errors[storeKey] : undefined ; | |
}, | |
/** | |
Returns the `SC.StoreError` object associated with a specific query. | |
@param {SC.Query} query The SC.Query with which the error is associated. | |
@returns {SC.StoreError} SC.StoreError or undefined if no error associated with the query. | |
*/ | |
readQueryError: function(query) { | |
var errors = this.queryErrors ; | |
return errors ? errors[SC.guidFor(query)] : undefined ; | |
}, | |
// .......................................................... | |
// DATA SOURCE CALLBACKS | |
// | |
// Mathods called by the data source on the store | |
/** | |
Called by a `dataSource` when it cancels an inflight operation on a | |
record. This will transition the record back to it non-inflight state. | |
@param {Number} storeKey record store key to cancel | |
@returns {SC.Store} reciever | |
*/ | |
dataSourceDidCancel: function(storeKey) { | |
var status = this.readStatus(storeKey), | |
K = SC.Record; | |
// EMPTY, ERROR, READY_CLEAN, READY_NEW, READY_DIRTY, DESTROYED_CLEAN, | |
// DESTROYED_DIRTY | |
if (!(status & K.BUSY)) { | |
throw K.BAD_STATE_ERROR; // should never be called in this state | |
} | |
// otherwise, determine proper state transition | |
switch(status) { | |
case K.BUSY_LOADING: | |
status = K.EMPTY; | |
break ; | |
case K.BUSY_CREATING: | |
status = K.READY_NEW; | |
break; | |
case K.BUSY_COMMITTING: | |
status = K.READY_DIRTY ; | |
break; | |
case K.BUSY_REFRESH_CLEAN: | |
status = K.READY_CLEAN; | |
break; | |
case K.BUSY_REFRESH_DIRTY: | |
status = K.READY_DIRTY ; | |
break ; | |
case K.BUSY_DESTROYING: | |
status = K.DESTROYED_DIRTY ; | |
break; | |
default: | |
throw K.BAD_STATE_ERROR ; | |
} | |
this.writeStatus(storeKey, status) ; | |
this.dataHashDidChange(storeKey, null, YES); | |
this._cancelCallback(storeKey); | |
return this ; | |
}, | |
/** | |
Called by a data source when it creates or commits a record. Passing an | |
optional id will remap the `storeKey` to the new record id. This is | |
required when you commit a record that does not have an id yet. | |
@param {Number} storeKey record store key to change to READY_CLEAN state | |
@param {Hash} dataHash optional data hash to replace current hash | |
@param {Object} newId optional new id to replace the old one | |
@returns {SC.Store} reciever | |
*/ | |
dataSourceDidComplete: function(storeKey, dataHash, newId) { | |
var status = this.readStatus(storeKey), K = SC.Record, statusOnly; | |
// EMPTY, ERROR, READY_CLEAN, READY_NEW, READY_DIRTY, DESTROYED_CLEAN, | |
// DESTROYED_DIRTY | |
if (!(status & K.BUSY)) { | |
throw K.BAD_STATE_ERROR; // should never be called in this state | |
} | |
// otherwise, determine proper state transition | |
if(status===K.BUSY_DESTROYING) { | |
throw K.BAD_STATE_ERROR ; | |
} else status = K.READY_CLEAN ; | |
this.writeStatus(storeKey, status) ; | |
if (dataHash) this.writeDataHash(storeKey, dataHash, status) ; | |
if (newId) SC.Store.replaceIdFor(storeKey, newId); | |
statusOnly = dataHash || newId ? NO : YES; | |
this.dataHashDidChange(storeKey, null, statusOnly); | |
// Force record to refresh its cached properties based on store key | |
var record = this.materializeRecord(storeKey); | |
if (!none(record)) { | |
record.notifyPropertyChange('status'); | |
} | |
//update callbacks | |
this._retreiveCallbackForStoreKey(storeKey); | |
return this ; | |
}, | |
/** | |
Called by a data source when it has destroyed a record. This will | |
transition the record to the proper state. | |
@param {Number} storeKey record store key to cancel | |
@returns {SC.Store} reciever | |
*/ | |
dataSourceDidDestroy: function(storeKey) { | |
var status = this.readStatus(storeKey), K = SC.Record; | |
// EMPTY, ERROR, READY_CLEAN, READY_NEW, READY_DIRTY, DESTROYED_CLEAN, | |
// DESTROYED_DIRTY | |
if (!(status & K.BUSY)) { | |
throw K.BAD_STATE_ERROR; // should never be called in this state | |
} | |
// otherwise, determine proper state transition | |
else{ | |
status = K.DESTROYED_CLEAN ; | |
} | |
this.removeDataHash(storeKey, status) ; | |
this.dataHashDidChange(storeKey); | |
// Force record to refresh its cached properties based on store key | |
var record = this.materializeRecord(storeKey); | |
if (!none(record)) { | |
record.notifyPropertyChange('status'); | |
} | |
this._retreiveCallbackForStoreKey(storeKey); | |
return this ; | |
}, | |
/** | |
Converts the passed record into an error object. | |
@param {Number} storeKey record store key to error | |
@param {SC.StoreError} error [optional] an SC.StoreError instance to associate with storeKey | |
@returns {SC.Store} reciever | |
*/ | |
dataSourceDidError: function(storeKey, error) { | |
var status = this.readStatus(storeKey), errors = this.recordErrors, K = SC.Record; | |
// EMPTY, ERROR, READY_CLEAN, READY_NEW, READY_DIRTY, DESTROYED_CLEAN, | |
// DESTROYED_DIRTY | |
if (!(status & K.BUSY)) { throw K.BAD_STATE_ERROR; } | |
// otherwise, determine proper state transition | |
else status = K.ERROR ; | |
// Add the error to the array of record errors (for lookup later on if necessary). | |
if (error && error.isError) { | |
if (!errors) errors = this.recordErrors = []; | |
errors[storeKey] = error; | |
} | |
this.writeStatus(storeKey, status) ; | |
this.dataHashDidChange(storeKey, null, YES); | |
// Force record to refresh its cached properties based on store key | |
var record = this.materializeRecord(storeKey); | |
if (!none(record)) { | |
record.notifyPropertyChange('status'); | |
} | |
this._retreiveCallbackForStoreKey(storeKey); | |
return this ; | |
}, | |
// .......................................................... | |
// PUSH CHANGES FROM DATA SOURCE | |
// | |
/** | |
Call by the data source whenever you want to push new data out of band | |
into the store. | |
@param {Class} recordType the SC.Record subclass | |
@param {Object} id the record id or null | |
@param {Hash} dataHash data hash to load | |
@param {Number} storeKey optional store key. | |
@returns {Number|Boolean} storeKey if push was allowed, NO if not | |
*/ | |
pushRetrieve: function(recordType, id, dataHash, storeKey) { | |
var K = SC.Record, status; | |
if(storeKey===undefined) storeKey = recordType.storeKeyFor(id); | |
status = this.readStatus(storeKey); | |
if(status==K.EMPTY || status==K.ERROR || status==K.READY_CLEAN || status==K.DESTROYED_CLEAN) { | |
status = K.READY_CLEAN; | |
if(dataHash===undefined) this.writeStatus(storeKey, status) ; | |
else this.writeDataHash(storeKey, dataHash, status) ; | |
this.dataHashDidChange(storeKey); | |
return storeKey; | |
} | |
//conflicted (ready) | |
return NO; | |
}, | |
/** | |
Call by the data source whenever you want to push a deletion into the | |
store. | |
@param {Class} recordType the SC.Record subclass | |
@param {Object} id the record id or null | |
@param {Number} storeKey optional store key. | |
@returns {Number|Boolean} storeKey if push was allowed, NO if not | |
*/ | |
pushDestroy: function(recordType, id, storeKey) { | |
var K = SC.Record, status; | |
if(storeKey===undefined){ | |
storeKey = recordType.storeKeyFor(id); | |
} | |
status = this.readStatus(storeKey); | |
if(status==K.EMPTY || status==K.ERROR || status==K.READY_CLEAN || status==K.DESTROYED_CLEAN){ | |
status = K.DESTROYED_CLEAN; | |
this.removeDataHash(storeKey, status) ; | |
this.dataHashDidChange(storeKey); | |
return storeKey; | |
} | |
//conflicted (destroy) | |
return NO; | |
}, | |
/** | |
Call by the data source whenever you want to push an error into the | |
store. | |
@param {Class} recordType the SC.Record subclass | |
@param {Object} id the record id or null | |
@param {SC.StoreError} error [optional] an SC.StoreError instance to associate with id or storeKey | |
@param {Number} storeKey optional store key. | |
@returns {Number|Boolean} storeKey if push was allowed, NO if not | |
*/ | |
pushError: function(recordType, id, error, storeKey) { | |
var K = SC.Record, status, errors = this.recordErrors; | |
if(storeKey===undefined) storeKey = recordType.storeKeyFor(id); | |
status = this.readStatus(storeKey); | |
if(status==K.EMPTY || status==K.ERROR || status==K.READY_CLEAN || status==K.DESTROYED_CLEAN){ | |
status = K.ERROR; | |
// Add the error to the array of record errors (for lookup later on if necessary). | |
if (error && error.isError) { | |
if (!errors) errors = this.recordErrors = []; | |
errors[storeKey] = error; | |
} | |
this.writeStatus(storeKey, status) ; | |
this.dataHashDidChange(storeKey, null, YES); | |
return storeKey; | |
} | |
//conflicted (error) | |
return NO; | |
}, | |
// .......................................................... | |
// FETCH CALLBACKS | |
// | |
// **NOTE**: although these method works on RecordArray instances right now. | |
// They could be optimized to actually share query results between nested | |
// stores. This is why these methods are implemented here instead of | |
// directly on `Query` or `RecordArray` objects. | |
/** | |
Sets the passed array of storeKeys as the new data for the query. You | |
can call this at any time for a remote query to update its content. If | |
you want to use incremental loading, then pass a `SparseArray` object. | |
If the query you pass is not a REMOTE query, then this method will raise | |
an exception. This will also implicitly transition the query state to | |
`SC.Record.READY`. | |
If you called `loadRecords()` before to load the actual content, you can | |
call this method with the return value of that method to actually set the | |
storeKeys on the result. | |
@param {SC.Query} query the query you are loading. must be remote. | |
@param {SC.Array} storeKeys array of store keys | |
@returns {SC.Store} receiver | |
*/ | |
loadQueryResults: function(query, storeKeys) { | |
if (get(query, 'location') === SC.Query.LOCAL) { | |
throw new Error("Cannot load query results for a local query"); | |
} | |
var recArray = this._findQuery(query, YES, NO); | |
if (recArray) set(recArray, 'storeKeys', storeKeys); | |
this.dataSourceDidFetchQuery(query); | |
return this ; | |
}, | |
/** | |
Called by your data source whenever you finish fetching the results of a | |
query. This will put the query into a READY state if it was loading. | |
Note that if the query is a REMOTE query, then you must separately load | |
the results into the query using `loadQueryResults()`. If the query is | |
LOCAL, then the query will update automatically with any new records you | |
added to the store. | |
@param {SC.Query} query the query you fetched | |
@returns {SC.Store} receiver | |
*/ | |
dataSourceDidFetchQuery: function(query) { | |
return this._scstore_dataSourceDidFetchQuery(query, YES); | |
}, | |
_scstore_dataSourceDidFetchQuery: 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.storeDidFetchQuery(query); | |
// notify nested stores | |
while(--loc >= 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 {SC.Query} query the query you cancelled | |
@returns {SC.Store} receiver | |
*/ | |
dataSourceDidCancelQuery: function(query) { | |
return this._scstore_dataSourceDidCancelQuery(query, YES); | |
}, | |
_scstore_dataSourceDidCancelQuery: function(query, createIfNeeded) { | |
var recArray = this._findQuery(query, createIfNeeded, NO), | |
nestedStores = 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 {SC.Query} query the query with the error | |
@param {SC.StoreError} error [optional] an SC.StoreError instance to associate with query | |
@returns {SC.Store} receiver | |
*/ | |
dataSourceDidErrorQuery: function(query, error) { | |
var errors = this.queryErrors; | |
// Add the error to the array of query errors (for lookup later on if necessary). | |
if (error && error.isError) { | |
if (!errors) errors = this.queryErrors = {}; | |
errors[SC.guidFor(query)] = error; | |
} | |
return this._scstore_dataSourceDidErrorQuery(query, YES); | |
}, | |
_scstore_dataSourceDidErrorQuery: function(query, createIfNeeded) { | |
var recArray = this._findQuery(query, createIfNeeded, NO), | |
nestedStores = 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 SC.Store.idFor(storeKey); | |
}, | |
/** | |
Given a storeKey, return the recordType. | |
@param {Number} storeKey the store key | |
@returns {SC.Record} record instance | |
*/ | |
recordTypeFor: function(storeKey) { | |
return SC.Store.recordTypeFor(storeKey) ; | |
}, | |
/** | |
Given a `recordType` and `primaryKey`, find the `storeKey`. If the | |
`primaryKey` has not been assigned a `storeKey` yet, it will be added. | |
@param {SC.Record} recordType the record type | |
@param {String} primaryKey the primary key | |
@returns {Number} storeKey | |
*/ | |
storeKeyFor: function(recordType, primaryKey) { | |
return recordType.storeKeyFor(primaryKey); | |
}, | |
/** | |
Given a `primaryKey` value for the record, returns the associated | |
`storeKey`. As opposed to `storeKeyFor()` however, this method | |
will **NOT** generate a new `storeKey` but returned `undefined`. | |
@param {SC.Record} recordType the record type | |
@param {String} primaryKey the primary key | |
@returns {Number} a storeKey. | |
*/ | |
storeKeyExists: function(recordType, primaryKey) { | |
return recordType.storeKeyExists(primaryKey); | |
}, | |
/** | |
Finds all `storeKey`s of a certain record type in this store | |
and returns an array. | |
@param {SC.Record} recordType | |
@returns {Array} set of storeKeys | |
*/ | |
storeKeysFor: function(recordType) { | |
var ret = [], | |
isEnum = recordType && recordType.isEnumerable, | |
recType, storeKey, isMatch ; | |
if (!this.statuses) return ret; | |
for(storeKey in SC.Store.recordTypesByStoreKey) { | |
recType = SC.Store.recordTypesByStoreKey[storeKey]; | |
// if same record type and this store has it | |
if (isEnum) isMatch = recordType.contains(recType); | |
else isMatch = recType === recordType; | |
if(isMatch && this.statuses[storeKey]) ret.push(parseInt(storeKey, 10)); | |
} | |
return ret; | |
}, | |
/** | |
Finds all `storeKey`s in this store | |
and returns an array. | |
@returns {Array} set of storeKeys | |
*/ | |
storeKeys: function() { | |
var ret = [], storeKey; | |
if(!this.statuses) return ret; | |
for(storeKey in this.statuses) { | |
// if status is not empty | |
if(this.statuses[storeKey] != SC.Record.EMPTY) { | |
ret.push(parseInt(storeKey, 10)); | |
} | |
} | |
return ret; | |
}, | |
/** | |
Returns string representation of a `storeKey`, with status. | |
@param {Number} storeKey | |
@returns {String} | |
*/ | |
statusString: function(storeKey) { | |
var rec = this.materializeRecord(storeKey); | |
return rec.statusString(); | |
} | |
}) ; | |
SC.Store.reopenClass(/** @scope SC.Store.prototype */{ | |
/** | |
Standard error raised if you try to commit changes from a nested store | |
and there is a conflict. | |
@type Error | |
*/ | |
CHAIN_CONFLICT_ERROR: new Error("Nested Store Conflict"), | |
/** | |
Standard error if you try to perform an operation on a nested store | |
without a parent. | |
@type Error | |
*/ | |
NO_PARENT_STORE_ERROR: new Error("Parent Store Required"), | |
/** | |
Standard error if you try to perform an operation on a nested store that | |
is only supported in root stores. | |
@type Error | |
*/ | |
NESTED_STORE_UNSUPPORTED_ERROR: new Error("Unsupported In Nested Store"), | |
/** | |
Standard error if you try to retrieve a record in a nested store that is | |
dirty. (This is allowed on the main store, but not in nested stores.) | |
@type Error | |
*/ | |
NESTED_STORE_RETRIEVE_DIRTY_ERROR: new Error("Cannot Retrieve Dirty Record in Nested Store"), | |
/** | |
Data hash state indicates the data hash is currently editable | |
@type String | |
*/ | |
EDITABLE: 'editable', | |
/** | |
Data hash state indicates the hash no longer tracks changes from a | |
parent store, but it is not editable. | |
@type String | |
*/ | |
LOCKED: 'locked', | |
/** | |
Data hash state indicates the hash is tracking changes from the parent | |
store and is not editable. | |
@type String | |
*/ | |
INHERITED: 'inherited', | |
/** @private | |
This array maps all storeKeys to primary keys. You will not normally | |
access this method directly. Instead use the `idFor()` and | |
`storeKeyFor()` methods on `SC.Record`. | |
*/ | |
idsByStoreKey: [], | |
/** @private | |
Maps all `storeKey`s to a `recordType`. Once a `storeKey` is associated | |
with a `primaryKey` and `recordType` that remains constant throughout the | |
lifetime of the application. | |
*/ | |
recordTypesByStoreKey: {}, | |
/** @private | |
Maps some `storeKeys` to query instance. Once a `storeKey` is associated | |
with a query instance, that remains constant through the lifetime of the | |
application. If a `Query` is destroyed, it will remove itself from this | |
list. | |
Don't access this directly. Use queryFor(). | |
*/ | |
queriesByStoreKey: [], | |
/** @private | |
The next store key to allocate. A storeKey must always be greater than 0 | |
*/ | |
nextStoreKey: 1, | |
/** | |
Generates a new store key for use. | |
@type Number | |
*/ | |
generateStoreKey: function() { return this.nextStoreKey++; }, | |
/** | |
Given a `storeKey` returns the `primaryKey` associated with the key. | |
If no `primaryKey` is associated with the `storeKey`, returns `null`. | |
@param {Number} storeKey the store key | |
@returns {String} the primary key or null | |
*/ | |
idFor: function(storeKey) { | |
return this.idsByStoreKey[storeKey] ; | |
}, | |
/** | |
Given a `storeKey`, returns the query object associated with the key. If | |
no query is associated with the `storeKey`, returns `null`. | |
@param {Number} storeKey the store key | |
@returns {SC.Query} query query object | |
*/ | |
queryFor: function(storeKey) { | |
return this.queriesByStoreKey[storeKey]; | |
}, | |
/** | |
Given a `storeKey` returns the `SC.Record` class associated with the key. | |
If no record type is associated with the store key, returns `null`. | |
The SC.Record class will only be found if you have already called | |
storeKeyFor() on the record. | |
@param {Number} storeKey the store key | |
@returns {SC.Record} the record type | |
*/ | |
recordTypeFor: function(storeKey) { | |
return this.recordTypesByStoreKey[storeKey]; | |
}, | |
/** | |
Swaps the `primaryKey` mapped to the given storeKey with the new | |
`primaryKey`. If the `storeKey` is not currently associated with a record | |
this will raise an exception. | |
@param {Number} storeKey the existing store key | |
@param {String} newPrimaryKey the new primary key | |
@returns {SC.Store} receiver | |
*/ | |
replaceIdFor: function(storeKey, newId) { | |
var oldId = this.idsByStoreKey[storeKey], | |
recordType, storeKeys; | |
if (oldId !== newId) { // skip if id isn't changing | |
recordType = this.recordTypeFor(storeKey); | |
if (!recordType) { | |
throw new Error("replaceIdFor: storeKey %@ does not exist".fmt(storeKey)); | |
} | |
// map one direction... | |
this.idsByStoreKey[storeKey] = newId; | |
// then the other... | |
storeKeys = recordType.storeKeysById() ; | |
delete storeKeys[oldId]; | |
storeKeys[newId] = storeKey; | |
} | |
return this ; | |
}, | |
/** | |
Swaps the `recordType` recorded for a given `storeKey`. Normally you | |
should not call this method directly as it can damage the store behavior. | |
This method is used by other store methods to set the `recordType` for a | |
`storeKey`. | |
@param {Integer} storeKey the store key | |
@param {SC.Record} recordType a record class | |
@returns {SC.Store} reciever | |
*/ | |
replaceRecordTypeFor: function(storeKey, recordType) { | |
this.recordTypesByStoreKey[storeKey] = recordType; | |
return this ; | |
} | |
}); | |
/** @private */ | |
SC.Store.reopen({ | |
nextStoreIndex: 1 | |
}); | |
// .......................................................... | |
// COMPATIBILITY | |
// | |
/** @private | |
global store is used only for deprecated compatibility methods. Don't use | |
this in real code. | |
*/ | |
SC.Store._getDefaultStore = function() { | |
var store = this._store; | |
if(!store) this._store = store = SC.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 `SC.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 {SC.Record} recordType default record type | |
@param {Boolean} isLoaded ignored | |
@returns {Array} SC.Record instances for loaded data hashes | |
*/ | |
SC.Store.updateRecords = function(dataHashes, dataSource, recordType, isLoaded) { | |
SC.Logger.warn("SC.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<len;idx++) recordType[idx] = dataHashes[idx].recordType; | |
} | |
// call new API. Returns storeKeys | |
ret = store.loadRecords(recordType, dataHashes); | |
// map to SC.Record instances | |
len = ret.length; | |
for(idx=0;idx<len;idx++) ret[idx] = store.materializeRecord(ret[idx]); | |
return ret ; | |
}; | |
/** @private | |
DEPRECATED | |
Finds a record with the passed guid on the default store. This is included | |
only for compatibility. You should use the newer `find()` method defined on | |
`SC.Store` instead. | |
@param {String} guid the guid | |
@param {SC.Record} recordType expected record type | |
@returns {SC.Record} found record | |
*/ | |
SC.Store.find = function(guid, recordType) { | |
return this._getDefaultStore().find(recordType, guid); | |
}; | |
/** @private | |
DEPRECATED | |
Passes through to `findAll` on default store. This is included only for | |
compatibility. You should use the newer `findAll()` defined on `SC.Store` | |
instead. | |
@param {Hash} filter search parameters | |
@param {SC.Record} recordType type of record to find | |
@returns {SC.RecordArray} result set | |
*/ | |
SC.Store.findAll = function(filter, recordType) { | |
return this._getDefaultStore().findAll(filter, recordType); | |
}; | |
})({}); | |
(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 */ | |
require('sproutcore-datastore/system/store'); | |
var get = SC.get, set = SC.set; | |
var o_create = SC.platform.create; | |
/** | |
@class | |
A nested store can buffer changes to a parent store and then commit them | |
all at once. You usually will use a `NestedStore` as part of store chaining | |
to stage changes to your object graph before sharing them with the rest of | |
the application. | |
Normally you will not create a nested store directly. Instead, you can | |
retrieve a nested store by using the `chain()` method. When you are finished | |
working with the nested store, `destroy()` will dispose of it. | |
@extends SC.Store | |
@since SproutCore 1.0 | |
*/ | |
SC.NestedStore = SC.Store.extend( | |
/** @scope SC.NestedStore.prototype */ { | |
/** | |
This is set to YES when there are changes that have not been committed | |
yet. | |
@type Boolean | |
@default NO | |
*/ | |
hasChanges: NO, | |
/** | |
The parent store this nested store is chained to. Nested stores must have | |
a parent store in order to function properly. Normally, you create a | |
nested store using the `SC.Store#chain()` method and this property will be | |
set for you. | |
@type SC.Store | |
@default null | |
*/ | |
parentStore: null, | |
/** | |
`YES` if the view is nested. Walk like a duck | |
@type Boolean | |
@default YES | |
*/ | |
isNested: YES, | |
/** | |
If YES, then the attribute hash state will be locked when you first | |
read the data hash or status. This means that if you retrieve a record | |
then change the record in the parent store, the changes will not be | |
visible to your nested store until you commit or discard changes. | |
If `NO`, then the attribute hash will lock only when you write data. | |
Normally you want to lock your attribute hash the first time you read it. | |
This will make your nested store behave most consistently. However, if | |
you are using multiple sibling nested stores at one time, you may want | |
to turn off this property so that changes from one store will be reflected | |
in the other one immediately. In this case you will be responsible for | |
ensuring that the sibling stores do not edit the same part of the object | |
graph at the same time. | |
@type Boolean | |
@default YES | |
*/ | |
lockOnRead: YES, | |
/** @private | |
Array contains the base revision for an attribute hash when it was first | |
cloned from the parent store. If the attribute hash is edited and | |
commited, the commit will fail if the parent attributes hash has been | |
edited since. | |
This is a form of optimistic locking, hence the name. | |
Each store gets its own array of locks, which are selectively populated | |
as needed. | |
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 | |
@default null | |
*/ | |
locks: null, | |
/** @private | |
An array that includes the store keys that have changed since the store | |
was last committed. This array is used to sync data hash changes between | |
chained stores. For a log changes that may actually be committed back to | |
the server see the changelog property. | |
@type SC.Set | |
@default YES | |
*/ | |
chainedChanges: null, | |
// .......................................................... | |
// STORE CHAINING | |
// | |
/** | |
`find()` cannot accept REMOTE queries in a nested store. This override will | |
verify that condition for you. See `SC.Store#find()` for info on using this | |
method. | |
@param {SC.Query} query query object to use. | |
@returns {SC.Record|SC.RecordArray} | |
*/ | |
find: function(query) { | |
sc_assert("SC.Store#find() can only accept LOCAL queries in nested stores", | |
!query || !(query instanceof SC.Query) || get(query, 'location') === SC.Query.LOCAL); | |
return this._super.apply(this, arguments); | |
}, | |
/** | |
Propagate this store's changes to its parent. If the store does not | |
have a parent, this has no effect other than to clear the change set. | |
@param {Boolean} force if YES, does not check for conflicts first | |
@returns {SC.Store} receiver | |
*/ | |
commitChanges: function(force) { | |
if (get(this, 'hasChanges')) { | |
var pstore = get(this, 'parentStore'); | |
pstore.commitChangesFromNestedStore(this, this.chainedChanges, force); | |
} | |
// clear out custom changes - even if there is nothing to commit. | |
this.reset(); | |
return this ; | |
}, | |
/** | |
Discard the changes made to this store and reset the store. | |
@returns {SC.Store} receiver | |
*/ | |
discardChanges: function() { | |
// any locked records whose rev or lock rev differs from parent need to | |
// be notified. | |
var records, locks; | |
if ((records = this.records) && (locks = this.locks)) { | |
var pstore = get(this, 'parentStore'), psRevisions = pstore.revisions; | |
var revisions = this.revisions, storeKey, lock, rev; | |
for (storeKey in records) { | |
if (!records.hasOwnProperty(storeKey)) continue ; | |
if (!(lock = locks[storeKey])) continue; // not locked. | |
rev = psRevisions[storeKey]; | |
if ((rev !== lock) || (revisions[storeKey] > 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 {SC.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 SC.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 `SC.StoreError` object associated with a specific record. | |
Delegates the call to the parent store. | |
@param {Number} storeKey The store key of the record. | |
@returns {SC.StoreError} SC.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 `SC.StoreError` object associated with a specific query. | |
Delegates the call to the parent store. | |
@param {SC.Query} query The SC.Query with which the error is associated. | |
@returns {SC.StoreError} SC.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]) ? SC.Store.EDITABLE : (locks && locks[storeKey]) ? SC.Store.LOCKED : SC.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)) === SC.Store.INHERITED) { | |
pstore = get(pstore, 'parentStore'); | |
} | |
if (pstore && editState === SC.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 ? SC.getPath(this.dataHashes[pk], path) : null; | |
} | |
} | |
else { | |
this.dataHashes[storeKey] = SC.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] || SC.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 = SC.Store.generateStoreKey(); | |
var isArray, len, idx, storeKey; | |
isArray = SC.typeOf(storeKeys) === 'array'; | |
if (isArray) { | |
len = storeKeys.length; | |
} else { | |
len = 1; | |
storeKey = storeKeys; | |
} | |
var changes = this.chainedChanges; | |
if (!changes) changes = this.chainedChanges = SC.Set.create(); | |
for(idx=0;idx<len;idx++) { | |
if (isArray) storeKey = storeKeys[idx]; | |
this._lock(storeKey); | |
this.revisions[storeKey] = rev; | |
changes.add(storeKey); | |
this._notifyRecordPropertyChange(storeKey, statusOnly, key); | |
} | |
set(this, 'hasChanges', YES); | |
return this ; | |
}, | |
// .......................................................... | |
// SYNCING CHANGES | |
// | |
/** @private - adapt for nested store */ | |
commitChangesFromNestedStore: function(nestedStore, changes, force) { | |
this._super(nestedStore, changes, force); | |
// save a lock for each store key if it does not have one already | |
// also add each storeKey to my own changes set. | |
var pstore = get(this, 'parentStore'), psRevisions = pstore.revisions, i; | |
var myLocks = this.locks, myChanges = this.chainedChanges,len,storeKey; | |
if (!myLocks) myLocks = this.locks = []; | |
if (!myChanges) myChanges = this.chainedChanges = SC.Set.create(); | |
len = changes.length ; | |
for(i=0;i<len;i++) { | |
storeKey = changes[i]; | |
if (!myLocks[storeKey]) myLocks[storeKey] = psRevisions[storeKey]||1; | |
myChanges.add(storeKey); | |
} | |
// Finally, mark store as dirty if we have changes | |
set(this, 'hasChanges', get(myChanges, 'length')>0); | |
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 = SC.Record, status; | |
// Is this a refresh? | |
if (isRefresh) { | |
for(idx=0;idx<len;idx++) { | |
storeKey = !storeKeys ? pstore.storeKeyFor(recordTypes, ids[idx]) : storeKeys[idx]; | |
status = this.peekStatus(storeKey); | |
// We won't allow calling retrieve on a dirty record in a nested store | |
// (although we do allow it in the main store). This is because doing | |
// so would involve writing a unique status, and that would break the | |
// status hierarchy, so even though lower stores would complete the | |
// retrieval, the upper layers would never inherit the new statuses. | |
if (status & K.DIRTY) { | |
throw SC.Store.NESTED_STORE_RETRIEVE_DIRTY_ERROR; | |
} | |
else { | |
// Not dirty? Then abandon any status we had set (to re-establish | |
// any prototype linkage breakage) before asking our parent store to | |
// perform the retrieve. | |
var dataHashes = this.dataHashes, | |
revisions = this.revisions, | |
statuses = this.statuses, | |
editables = this.editables, | |
locks = this.locks; | |
var changed = NO; | |
var statusOnly = NO; | |
if (dataHashes && dataHashes.hasOwnProperty(storeKey)) { | |
delete dataHashes[storeKey]; | |
changed = YES; | |
} | |
if (revisions && revisions.hasOwnProperty(storeKey)) { | |
delete revisions[storeKey]; | |
changed = YES; | |
} | |
if (editables) delete editables[storeKey]; | |
if (locks) delete locks[storeKey]; | |
if (statuses && statuses.hasOwnProperty(storeKey)) { | |
delete statuses[storeKey]; | |
if (!changed) statusOnly = YES; | |
changed = YES; | |
} | |
if (changed) this._notifyRecordPropertyChange(storeKey, statusOnly); | |
} | |
} | |
} | |
return pstore.retrieveRecords(recordTypes, ids, storeKeys, isRefresh); | |
}, | |
/** @private - adapt for nested store */ | |
commitRecords: function(recordTypes, ids, storeKeys) { | |
throw SC.Store.NESTED_STORE_UNSUPPORTED_ERROR; | |
}, | |
/** @private - adapt for nested store */ | |
commitRecord: function(recordType, id, storeKey) { | |
throw SC.Store.NESTED_STORE_UNSUPPORTED_ERROR; | |
}, | |
/** @private - adapt for nested store */ | |
cancelRecords: function(recordTypes, ids, storeKeys) { | |
throw SC.Store.NESTED_STORE_UNSUPPORTED_ERROR; | |
}, | |
/** @private - adapt for nested store */ | |
cancelRecord: function(recordType, id, storeKey) { | |
throw SC.Store.NESTED_STORE_UNSUPPORTED_ERROR; | |
}, | |
// .......................................................... | |
// DATA SOURCE CALLBACKS | |
// | |
// Mathods called by the data source on the store | |
/** @private - adapt for nested store */ | |
dataSourceDidCancel: function(storeKey) { | |
throw SC.Store.NESTED_STORE_UNSUPPORTED_ERROR; | |
}, | |
/** @private - adapt for nested store */ | |
dataSourceDidComplete: function(storeKey, dataHash, newId) { | |
throw SC.Store.NESTED_STORE_UNSUPPORTED_ERROR; | |
}, | |
/** @private - adapt for nested store */ | |
dataSourceDidDestroy: function(storeKey) { | |
throw SC.Store.NESTED_STORE_UNSUPPORTED_ERROR; | |
}, | |
/** @private - adapt for nested store */ | |
dataSourceDidError: function(storeKey, error) { | |
throw SC.Store.NESTED_STORE_UNSUPPORTED_ERROR; | |
}, | |
// .......................................................... | |
// PUSH CHANGES FROM DATA SOURCE | |
// | |
/** @private - adapt for nested store */ | |
pushRetrieve: function(recordType, id, dataHash, storeKey) { | |
throw SC.Store.NESTED_STORE_UNSUPPORTED_ERROR; | |
}, | |
/** @private - adapt for nested store */ | |
pushDestroy: function(recordType, id, storeKey) { | |
throw SC.Store.NESTED_STORE_UNSUPPORTED_ERROR; | |
}, | |
/** @private - adapt for nested store */ | |
pushError: function(recordType, id, error, storeKey) { | |
throw SC.Store.NESTED_STORE_UNSUPPORTED_ERROR; | |
} | |
}) ; | |
})({}); | |
(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) | |
// ========================================================================== | |
require('sproutcore-datastore/system/child_array'); | |
require('sproutcore-datastore/system/many_array'); | |
require('sproutcore-datastore/system/nested_store'); | |
require('sproutcore-datastore/system/query'); | |
require('sproutcore-datastore/system/record'); | |
require('sproutcore-datastore/system/record_array'); | |
require('sproutcore-datastore/system/store'); | |
require('sproutcore-datastore/system/store_error'); | |
})({}); | |
(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) | |
// ========================================================================== | |
require('sproutcore-runtime'); | |
require('sproutcore-datastore/attributes'); | |
require('sproutcore-datastore/data_sources'); | |
require('sproutcore-datastore/system'); | |
})({}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment