return klass !== Model && Model.detect(klass);
},
columnsForType: function(type) {
var columns = [{
name: 'id',
desc: 'Id'
}];
var count = 0;
var self = this;
get(type, 'attributes').forEach(function(meta, name) {
if (count++ > self.attributeLimit) { return false; }
var desc = capitalize(underscore(name).replace('_', ' '));
columns.push({ name: name, desc: desc });
});
return columns;
},
getRecords: function(type) {
return this.get('store').all(type);
},
getRecordColumnValues: function(record) {
var self = this, count = 0;
var columnValues = { id: get(record, 'id') };
record.eachAttribute(function(key) {
if (count++ > self.attributeLimit) {
return false;
}
var value = get(record, key);
columnValues[key] = value;
});
return columnValues;
},
getRecordKeywords: function(record) {
var keywords = [];
var keys = Ember.A(['id']);
record.eachAttribute(function(key) {
keys.push(key);
});
keys.forEach(function(key) {
keywords.push(get(record, key));
});
return keywords;
},
getRecordFilterValues: function(record) {
return {
isNew: record.get('isNew'),
isModified: record.get('isDirty') && !record.get('isNew'),
isClean: !record.get('isDirty')
};
},
getRecordColor: function(record) {
var color = 'black';
if (record.get('isNew')) {
color = 'green';
} else if (record.get('isDirty')) {
color = 'blue';
}
return color;
},
observeRecord: function(record, recordUpdated) {
var releaseMethods = Ember.A(), self = this;
var keysToObserve = Ember.A(['id', 'isNew', 'isDirty']);
record.eachAttribute(function(key) {
keysToObserve.push(key);
});
keysToObserve.forEach(function(key) {
var handler = function() {
recordUpdated(self.wrapRecord(record));
};
Ember.addObserver(record, key, handler);
releaseMethods.push(function() {
Ember.removeObserver(record, key, handler);
});
});
var release = function() {
releaseMethods.forEach(function(fn) { fn(); } );
};
return release;
}
});
});
define("ember-data/system/debug/debug_info",
["ember-data/system/model","exports"],
function(__dependency1__, __exports__) {
"use strict";
var Model = __dependency1__.Model;
Model.reopen({
/**
Provides info about the model for debugging purposes
by grouping the properties into more semantic groups.
Meant to be used by debugging tools such as the Chrome Ember Extension.
- Groups all attributes in "Attributes" group.
- Groups all belongsTo relationships in "Belongs To" group.
- Groups all hasMany relationships in "Has Many" group.
- Groups all flags in "Flags" group.
- Flags relationship CPs as expensive properties.
@method _debugInfo
@for DS.Model
@private
*/
_debugInfo: function() {
var attributes = ['id'],
relationships = { belongsTo: [], hasMany: [] },
expensiveProperties = [];
this.eachAttribute(function(name, meta) {
attributes.push(name);
}, this);
this.eachRelationship(function(name, relationship) {
relationships[relationship.kind].push(name);
expensiveProperties.push(name);
});
var groups = [
{
name: 'Attributes',
properties: attributes,
expand: true
},
{
name: 'Belongs To',
properties: relationships.belongsTo,
expand: true
},
{
name: 'Has Many',
properties: relationships.hasMany,
expand: true
},
{
name: 'Flags',
properties: ['isLoaded', 'isDirty', 'isSaving', 'isDeleted', 'isError', 'isNew', 'isValid']
}
];
return {
propertyInfo: {
// include all other mixins / properties (not just the grouped ones)
includeOtherProperties: true,
groups: groups,
// don't pre-calculate unless cached
expensiveProperties: expensiveProperties
}
};
}
});
__exports__["default"] = Model;
});
define("ember-data/system/map",
["exports"],
function(__exports__) {
"use strict";
/**
* Polyfill Ember.Map behavior for Ember <= 1.7
* This can probably be removed before 1.0 final
*/
var mapForEach, deleteFn;
function OrderedSet(){
Ember.OrderedSet.apply(this, arguments);
}
function Map() {
Ember.Map.apply(this, arguments);
}
function MapWithDefault(){
Ember.MapWithDefault.apply(this, arguments);
}
var testMap = Ember.Map.create();
testMap.set('key', 'value');
var usesOldBehavior = false;
testMap.forEach(function(value, key){
usesOldBehavior = value === 'key' && key === 'value';
});
Map.prototype = Object.create(Ember.Map.prototype);
MapWithDefault.prototype = Object.create(Ember.MapWithDefault.prototype);
OrderedSet.prototype = Object.create(Ember.OrderedSet.prototype);
OrderedSet.create = function(){
return new OrderedSet();
};
/**
* returns a function that calls the original
* callback function in the correct order.
* if we are in pre-Ember.1.8 land, Map/MapWithDefault
* forEach calls with key, value, in that order.
* >= 1.8 forEach is called with the order value, key as per
* the ES6 spec.
*/
function translate(valueKeyOrderedCallback){
return function(key, value){
valueKeyOrderedCallback.call(this, value, key);
};
}
// old, non ES6 compliant behavior
if (usesOldBehavior){
mapForEach = function(callback, thisArg){
this.__super$forEach(translate(callback), thisArg);
};
/* alias to remove */
deleteFn = function(thing){
this.remove(thing);
};
Map.prototype.__super$forEach = Ember.Map.prototype.forEach;
Map.prototype.forEach = mapForEach;
Map.prototype["delete"] = deleteFn;
MapWithDefault.prototype.forEach = mapForEach;
MapWithDefault.prototype.__super$forEach = Ember.MapWithDefault.prototype.forEach;
MapWithDefault.prototype["delete"] = deleteFn;
OrderedSet.prototype["delete"] = deleteFn;
}
MapWithDefault.constructor = MapWithDefault;
Map.constructor = Map;
MapWithDefault.create = function(options){
if (options) {
return new MapWithDefault(options);
} else {
return new Map();
}
};
Map.create = function(){
return new this.constructor();
};
__exports__["default"] = Map;
__exports__.Map = Map;
__exports__.MapWithDefault = MapWithDefault;
__exports__.OrderedSet = OrderedSet;
});
define("ember-data/system/model",
["ember-data/system/model/model","ember-data/system/model/attributes","ember-data/system/model/states","ember-data/system/model/errors","exports"],
function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __exports__) {
"use strict";
/**
@module ember-data
*/
var Model = __dependency1__["default"];
var attr = __dependency2__["default"];
var RootState = __dependency3__["default"];
var Errors = __dependency4__["default"];
__exports__.Model = Model;
__exports__.RootState = RootState;
__exports__.attr = attr;
__exports__.Errors = Errors;
});
define("ember-data/system/model/attributes",
["ember-data/system/model/model","ember-data/system/map","exports"],
function(__dependency1__, __dependency2__, __exports__) {
"use strict";
var Model = __dependency1__["default"];
var Map = __dependency2__.Map;
/**
@module ember-data
*/
var get = Ember.get;
/**
@class Model
@namespace DS
*/
Model.reopenClass({
/**
A map whose keys are the attributes of the model (properties
described by DS.attr) and whose values are the meta object for the
property.
Example
```javascript
App.Person = DS.Model.extend({
firstName: attr('string'),
lastName: attr('string'),
birthday: attr('date')
});
var attributes = Ember.get(App.Person, 'attributes')
attributes.forEach(function(name, meta) {
console.log(name, meta);
});
// prints:
// firstName {type: "string", isAttribute: true, options: Object, parentType: function, name: "firstName"}
// lastName {type: "string", isAttribute: true, options: Object, parentType: function, name: "lastName"}
// birthday {type: "date", isAttribute: true, options: Object, parentType: function, name: "birthday"}
```
@property attributes
@static
@type {Ember.Map}
@readOnly
*/
attributes: Ember.computed(function() {
var map = Map.create();
this.eachComputedProperty(function(name, meta) {
if (meta.isAttribute) {
Ember.assert("You may not set `id` as an attribute on your model. Please remove any lines that look like: `id: DS.attr('<type>')` from " + this.toString(), name !== 'id');
meta.name = name;
map.set(name, meta);
}
});
return map;
}).readOnly(),
/**
A map whose keys are the attributes of the model (properties
described by DS.attr) and whose values are type of transformation
applied to each attribute. This map does not include any
attributes that do not have an transformation type.
Example
```javascript
App.Person = DS.Model.extend({
firstName: attr(),
lastName: attr('string'),
birthday: attr('date')
});
var transformedAttributes = Ember.get(App.Person, 'transformedAttributes')
transformedAttributes.forEach(function(field, type) {
console.log(field, type);
});
// prints:
// lastName string
// birthday date
```
@property transformedAttributes
@static
@type {Ember.Map}
@readOnly
*/
transformedAttributes: Ember.computed(function() {
var map = Map.create();
this.eachAttribute(function(key, meta) {
if (meta.type) {
map.set(key, meta.type);
}
});
return map;
}).readOnly(),
/**
Iterates through the attributes of the model, calling the passed function on each
attribute.
The callback method you provide should have the following signature (all
parameters are optional):
```javascript
function(name, meta);
```
- `name` the name of the current property in the iteration
- `meta` the meta object for the attribute property in the iteration
Note that in addition to a callback, you can also pass an optional target
object that will be set as `this` on the context.
Example
```javascript
App.Person = DS.Model.extend({
firstName: attr('string'),
lastName: attr('string'),
birthday: attr('date')
});
App.Person.eachAttribute(function(name, meta) {
console.log(name, meta);
});
// prints:
// firstName {type: "string", isAttribute: true, options: Object, parentType: function, name: "firstName"}
// lastName {type: "string", isAttribute: true, options: Object, parentType: function, name: "lastName"}
// birthday {type: "date", isAttribute: true, options: Object, parentType: function, name: "birthday"}
```
@method eachAttribute
@param {Function} callback The callback to execute
@param {Object} [target] The target object to use
@static
*/
eachAttribute: function(callback, binding) {
get(this, 'attributes').forEach(function(meta, name) {
callback.call(binding, name, meta);
}, binding);
},
/**
Iterates through the transformedAttributes of the model, calling
the passed function on each attribute. Note the callback will not be
called for any attributes that do not have an transformation type.
The callback method you provide should have the following signature (all
parameters are optional):
```javascript
function(name, type);
```
- `name` the name of the current property in the iteration
- `type` a string containing the name of the type of transformed
applied to the attribute
Note that in addition to a callback, you can also pass an optional target
object that will be set as `this` on the context.
Example
```javascript
App.Person = DS.Model.extend({
firstName: attr(),
lastName: attr('string'),
birthday: attr('date')
});
App.Person.eachTransformedAttribute(function(name, type) {
console.log(name, type);
});
// prints:
// lastName string
// birthday date
```
@method eachTransformedAttribute
@param {Function} callback The callback to execute
@param {Object} [target] The target object to use
@static
*/
eachTransformedAttribute: function(callback, binding) {
get(this, 'transformedAttributes').forEach(function(type, name) {
callback.call(binding, name, type);
});
}
});
Model.reopen({
eachAttribute: function(callback, binding) {
this.constructor.eachAttribute(callback, binding);
}
});
function getDefaultValue(record, options, key) {
if (typeof options.defaultValue === "function") {
return options.defaultValue.apply(null, arguments);
} else {
return options.defaultValue;
}
}
function hasValue(record, key) {
return record._attributes.hasOwnProperty(key) ||
record._inFlightAttributes.hasOwnProperty(key) ||
record._data.hasOwnProperty(key);
}
function getValue(record, key) {
if (record._attributes.hasOwnProperty(key)) {
return record._attributes[key];
} else if (record._inFlightAttributes.hasOwnProperty(key)) {
return record._inFlightAttributes[key];
} else {
return record._data[key];
}
}
/**
`DS.attr` defines an attribute on a [DS.Model](/api/data/classes/DS.Model.html).
By default, attributes are passed through as-is, however you can specify an
optional type to have the value automatically transformed.
Ember Data ships with four basic transform types: `string`, `number`,
`boolean` and `date`. You can define your own transforms by subclassing
[DS.Transform](/api/data/classes/DS.Transform.html).
Note that you cannot use `attr` to define an attribute of `id`.
`DS.attr` takes an optional hash as a second parameter, currently
supported options are:
- `defaultValue`: Pass a string or a function to be called to set the attribute
to a default value if none is supplied.
Example
```javascript
var attr = DS.attr;
App.User = DS.Model.extend({
username: attr('string'),
email: attr('string'),
verified: attr('boolean', {defaultValue: false})
});
```
@namespace
@method attr
@for DS
@param {String} type the attribute type
@param {Object} options a hash of options
@return {Attribute}
*/
__exports__["default"] = function attr(type, options) {
options = options || {};
var meta = {
type: type,
isAttribute: true,
options: options
};
return Ember.computed('data', function(key, value) {
if (arguments.length > 1) {
Ember.assert("You may not set `id` as an attribute on your model. Please remove any lines that look like: `id: DS.attr('<type>')` from " + this.constructor.toString(), key !== 'id');
var oldValue = getValue(this, key);
if (value !== oldValue) {
// Add the new value to the changed attributes hash; it will get deleted by
// the 'didSetProperty' handler if it is no different from the original value
this._attributes[key] = value;
this.send('didSetProperty', {
name: key,
oldValue: oldValue,
originalValue: this._data[key],
value: value
});
}
return value;
} else if (hasValue(this, key)) {
return getValue(this, key);
} else {
return getDefaultValue(this, options, key);
}
// `data` is never set directly. However, it may be
// invalidated from the state manager's setData
// event.
}).meta(meta);
};
});
define("ember-data/system/model/errors",
["ember-data/system/map","exports"],
function(__dependency1__, __exports__) {
"use strict";
var get = Ember.get;
var isEmpty = Ember.isEmpty;
var map = Ember.EnumerableUtils.map;
var MapWithDefault = __dependency1__.MapWithDefault;
/**
@module ember-data
*/
/**
Holds validation errors for a given record organized by attribute names.
Every DS.Model has an `errors` property that is an instance of
`DS.Errors`. This can be used to display validation error
messages returned from the server when a `record.save()` rejects.
This works automatically with `DS.ActiveModelAdapter`, but you
can implement [ajaxError](api/data/classes/DS.RESTAdapter.html#method_ajaxError)
in other adapters as well.
For Example, if you had an `User` model that looked like this:
```javascript
App.User = DS.Model.extend({
username: attr('string'),
email: attr('string')
});
```
And you attempted to save a record that did not validate on the backend.
```javascript
var user = store.createRecord('user', {
username: 'tomster',
email: 'invalidEmail'
});
user.save();
```
Your backend data store might return a response that looks like
this. This response will be used to populate the error object.
```javascript
{
"errors": {
"username": ["This username is already taken!"],
"email": ["Doesn't look like a valid email."]
}
}
```
Errors can be displayed to the user by accessing their property name
or using the `messages` property to get an array of all errors.
```handlebars
{{#each errors.messages}}
<div class="error">
{{message}}
</div>
{{/each}}
<label>Username: {{input value=username}} </label>
{{#each errors.username}}
<div class="error">
{{message}}
</div>
{{/each}}
<label>Email: {{input value=email}} </label>
{{#each errors.email}}
<div class="error">
{{message}}
</div>
{{/each}}
```
@class Errors
@namespace DS
@extends Ember.Object
@uses Ember.Enumerable
@uses Ember.Evented
*/
__exports__["default"] = Ember.Object.extend(Ember.Enumerable, Ember.Evented, {
/**
Register with target handler
@method registerHandlers
@param {Object} target
@param {Function} becameInvalid
@param {Function} becameValid
*/
registerHandlers: function(target, becameInvalid, becameValid) {
this.on('becameInvalid', target, becameInvalid);
this.on('becameValid', target, becameValid);
},
/**
@property errorsByAttributeName
@type {Ember.MapWithDefault}
@private
*/
errorsByAttributeName: Ember.reduceComputed("content", {
initialValue: function() {
return MapWithDefault.create({
defaultValue: function() {
return Ember.A();
}
});
},
addedItem: function(errors, error) {
errors.get(error.attribute).pushObject(error);
return errors;
},
removedItem: function(errors, error) {
errors.get(error.attribute).removeObject(error);
return errors;
}
}),
/**
Returns errors for a given attribute
```javascript
var user = store.createRecord('user', {
username: 'tomster',
email: 'invalidEmail'
});
user.save().catch(function(){
user.get('errors').errorsFor('email'); // ["Doesn't look like a valid email."]
});
```
@method errorsFor
@param {String} attribute
@return {Array}
*/
errorsFor: function(attribute) {
return get(this, 'errorsByAttributeName').get(attribute);
},
/**
An array containing all of the error messages for this
record. This is useful for displaying all errors to the user.
```handlebars
{{#each errors.messages}}
<div class="error">
{{message}}
</div>
{{/each}}
```
@property messages
@type {Array}
*/
messages: Ember.computed.mapBy('content', 'message'),
/**
@property content
@type {Array}
@private
*/
content: Ember.computed(function() {
return Ember.A();
}),
/**
@method unknownProperty
@private
*/
unknownProperty: function(attribute) {
var errors = this.errorsFor(attribute);
if (isEmpty(errors)) { return null; }
return errors;
},
/**
@method nextObject
@private
*/
nextObject: function(index, previousObject, context) {
return get(this, 'content').objectAt(index);
},
/**
Total number of errors.
@property length
@type {Number}
@readOnly
*/
length: Ember.computed.oneWay('content.length').readOnly(),
/**
@property isEmpty
@type {Boolean}
@readOnly
*/
isEmpty: Ember.computed.not('length').readOnly(),
/**
Adds error messages to a given attribute and sends
`becameInvalid` event to the record.
Example:
```javascript
if (!user.get('username') {
user.get('errors').add('username', 'This field is required');
}
```
@method add
@param {String} attribute
@param {Array|String} messages
*/
add: function(attribute, messages) {
var wasEmpty = get(this, 'isEmpty');
messages = this._findOrCreateMessages(attribute, messages);
get(this, 'content').addObjects(messages);
this.notifyPropertyChange(attribute);
this.enumerableContentDidChange();
if (wasEmpty && !get(this, 'isEmpty')) {
this.trigger('becameInvalid');
}
},
/**
@method _findOrCreateMessages
@private
*/
_findOrCreateMessages: function(attribute, messages) {
var errors = this.errorsFor(attribute);
return map(Ember.makeArray(messages), function(message) {
return errors.findBy('message', message) || {
attribute: attribute,
message: message
};
});
},
/**
Removes all error messages from the given attribute and sends
`becameValid` event to the record if there no more errors left.
Example:
```javascript
App.User = DS.Model.extend({
email: DS.attr('string'),
twoFactorAuth: DS.attr('boolean'),
phone: DS.attr('string')
});
App.UserEditRoute = Ember.Route.extend({
actions: {
save: function(user) {
if (!user.get('twoFactorAuth')) {
user.get('errors').remove('phone');
}
user.save();
}
}
});
```
@method remove
@param {String} attribute
*/
remove: function(attribute) {
if (get(this, 'isEmpty')) { return; }
var content = get(this, 'content').rejectBy('attribute', attribute);
get(this, 'content').setObjects(content);
this.notifyPropertyChange(attribute);
this.enumerableContentDidChange();
if (get(this, 'isEmpty')) {
this.trigger('becameValid');
}
},
/**
Removes all error messages and sends `becameValid` event
to the record.
Example:
```javascript
App.UserEditRoute = Ember.Route.extend({
actions: {
retrySave: function(user) {
user.get('errors').clear();
user.save();
}
}
});
```
@method clear
*/
clear: function() {
if (get(this, 'isEmpty')) { return; }
get(this, 'content').clear();
this.enumerableContentDidChange();
this.trigger('becameValid');
},
/**
Checks if there is error messages for the given attribute.
```javascript
App.UserEditRoute = Ember.Route.extend({
actions: {
save: function(user) {
if (user.get('errors').has('email')) {
return alert('Please update your email before attempting to save.');
}
user.save();
}
}
});
```
@method has
@param {String} attribute
@return {Boolean} true if there some errors on given attribute
*/
has: function(attribute) {
return !isEmpty(this.errorsFor(attribute));
}
});
});
define("ember-data/system/model/model",
["ember-data/system/model/states","ember-data/system/model/errors","ember-data/system/promise_proxies","ember-data/system/relationships/relationship","exports"],
function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __exports__) {
"use strict";
var RootState = __dependency1__["default"];
var Errors = __dependency2__["default"];
var PromiseObject = __dependency3__.PromiseObject;
var createRelationshipFor = __dependency4__.createRelationshipFor;
/**
@module ember-data
*/
var get = Ember.get;
var set = Ember.set;
var merge = Ember.merge;
var Promise = Ember.RSVP.Promise;
var forEach = Ember.ArrayPolyfills.forEach;
var map = Ember.ArrayPolyfills.map;
var JSONSerializer;
var retrieveFromCurrentState = Ember.computed('currentState', function(key, value) {
return get(get(this, 'currentState'), key);
}).readOnly();
var _extractPivotNameCache = Object.create(null);
var _splitOnDotCache = Object.create(null);
function splitOnDot(name) {
return _splitOnDotCache[name] || (
_splitOnDotCache[name] = name.split('.')
);
}
function extractPivotName(name) {
return _extractPivotNameCache[name] || (
_extractPivotNameCache[name] = splitOnDot(name)[0]
);
}
/**
The model class that all Ember Data records descend from.
@class Model
@namespace DS
@extends Ember.Object
@uses Ember.Evented
*/
var Model = Ember.Object.extend(Ember.Evented, {
_recordArrays: undefined,
_relationships: undefined,
_loadingRecordArrays: undefined,
/**
If this property is `true` the record is in the `empty`
state. Empty is the first state all records enter after they have
been created. Most records created by the store will quickly
transition to the `loading` state if data needs to be fetched from
the server or the `created` state if the record is created on the
client. A record can also enter the empty state if the adapter is
unable to locate the record.
@property isEmpty
@type {Boolean}
@readOnly
*/
isEmpty: retrieveFromCurrentState,
/**
If this property is `true` the record is in the `loading` state. A
record enters this state when the store asks the adapter for its
data. It remains in this state until the adapter provides the
requested data.
@property isLoading
@type {Boolean}
@readOnly
*/
isLoading: retrieveFromCurrentState,
/**
If this property is `true` the record is in the `loaded` state. A
record enters this state when its data is populated. Most of a
record's lifecycle is spent inside substates of the `loaded`
state.
Example
```javascript
var record = store.createRecord('model');
record.get('isLoaded'); // true
store.find('model', 1).then(function(model) {
model.get('isLoaded'); // true
});
```
@property isLoaded
@type {Boolean}
@readOnly
*/
isLoaded: retrieveFromCurrentState,
/**
If this property is `true` the record is in the `dirty` state. The
record has local changes that have not yet been saved by the
adapter. This includes records that have been created (but not yet
saved) or deleted.
Example
```javascript
var record = store.createRecord('model');
record.get('isDirty'); // true
store.find('model', 1).then(function(model) {
model.get('isDirty'); // false
model.set('foo', 'some value');
model.get('isDirty'); // true
});
```
@property isDirty
@type {Boolean}
@readOnly
*/
isDirty: retrieveFromCurrentState,
/**
If this property is `true` the record is in the `saving` state. A
record enters the saving state when `save` is called, but the
adapter has not yet acknowledged that the changes have been
persisted to the backend.
Example
```javascript
var record = store.createRecord('model');
record.get('isSaving'); // false
var promise = record.save();
record.get('isSaving'); // true
promise.then(function() {
record.get('isSaving'); // false
});
```
@property isSaving
@type {Boolean}
@readOnly
*/
isSaving: retrieveFromCurrentState,
/**
If this property is `true` the record is in the `deleted` state
and has been marked for deletion. When `isDeleted` is true and
`isDirty` is true, the record is deleted locally but the deletion
was not yet persisted. When `isSaving` is true, the change is
in-flight. When both `isDirty` and `isSaving` are false, the
change has persisted.
Example
```javascript
var record = store.createRecord('model');
record.get('isDeleted'); // false
record.deleteRecord();
// Locally deleted
record.get('isDeleted'); // true
record.get('isDirty'); // true
record.get('isSaving'); // false
// Persisting the deletion
var promise = record.save();
record.get('isDeleted'); // true
record.get('isSaving'); // true
// Deletion Persisted
promise.then(function() {
record.get('isDeleted'); // true
record.get('isSaving'); // false
record.get('isDirty'); // false
});
```
@property isDeleted
@type {Boolean}
@readOnly
*/
isDeleted: retrieveFromCurrentState,
/**
If this property is `true` the record is in the `new` state. A
record will be in the `new` state when it has been created on the
client and the adapter has not yet report that it was successfully
saved.
Example
```javascript
var record = store.createRecord('model');
record.get('isNew'); // true
record.save().then(function(model) {
model.get('isNew'); // false
});
```
@property isNew
@type {Boolean}
@readOnly
*/
isNew: retrieveFromCurrentState,
/**
If this property is `true` the record is in the `valid` state.
A record will be in the `valid` state when the adapter did not report any
server-side validation failures.
@property isValid
@type {Boolean}
@readOnly
*/
isValid: retrieveFromCurrentState,
/**
If the record is in the dirty state this property will report what
kind of change has caused it to move into the dirty
state. Possible values are:
- `created` The record has been created by the client and not yet saved to the adapter.
- `updated` The record has been updated by the client and not yet saved to the adapter.
- `deleted` The record has been deleted by the client and not yet saved to the adapter.
Example
```javascript
var record = store.createRecord('model');
record.get('dirtyType'); // 'created'
```
@property dirtyType
@type {String}
@readOnly
*/
dirtyType: retrieveFromCurrentState,
/**
If `true` the adapter reported that it was unable to save local
changes to the backend for any reason other than a server-side
validation error.
Example
```javascript
record.get('isError'); // false
record.set('foo', 'valid value');
record.save().then(null, function() {
record.get('isError'); // true
});
```
@property isError
@type {Boolean}
@readOnly
*/
isError: false,
/**
If `true` the store is attempting to reload the record form the adapter.
Example
```javascript
record.get('isReloading'); // false
record.reload();
record.get('isReloading'); // true
```
@property isReloading
@type {Boolean}
@readOnly
*/
isReloading: false,
/**
The `clientId` property is a transient numerical identifier
generated at runtime by the data store. It is important
primarily because newly created objects may not yet have an
externally generated id.
@property clientId
@private
@type {Number|String}
*/
clientId: null,
/**
All ember models have an id property. This is an identifier
managed by an external source. These are always coerced to be
strings before being used internally. Note when declaring the
attributes for a model it is an error to declare an id
attribute.
```javascript
var record = store.createRecord('model');
record.get('id'); // null
store.find('model', 1).then(function(model) {
model.get('id'); // '1'
});
```
@property id
@type {String}
*/
id: null,
/**
@property currentState
@private
@type {Object}
*/
currentState: RootState.empty,
/**
When the record is in the `invalid` state this object will contain
any errors returned by the adapter. When present the errors hash
typically contains keys corresponding to the invalid property names
and values which are an array of error messages.
```javascript
record.get('errors.length'); // 0
record.set('foo', 'invalid value');
record.save().then(null, function() {
record.get('errors').get('foo'); // ['foo should be a number.']
});
```
@property errors
@type {DS.Errors}
*/
errors: Ember.computed(function() {
var errors = Errors.create();
errors.registerHandlers(this, function() {
this.send('becameInvalid');
}, function() {
this.send('becameValid');
});
return errors;
}).readOnly(),
/**
Create a JSON representation of the record, using the serialization
strategy of the store's adapter.
`serialize` takes an optional hash as a parameter, currently
supported options are:
- `includeId`: `true` if the record's ID should be included in the
JSON representation.
@method serialize
@param {Object} options
@return {Object} an object whose values are primitive JSON values only
*/
serialize: function(options) {
var store = get(this, 'store');
return store.serialize(this, options);
},
/**
Use [DS.JSONSerializer](DS.JSONSerializer.html) to
get the JSON representation of a record.
`toJSON` takes an optional hash as a parameter, currently
supported options are:
- `includeId`: `true` if the record's ID should be included in the
JSON representation.
@method toJSON
@param {Object} options
@return {Object} A JSON representation of the object.
*/
toJSON: function(options) {
if (!JSONSerializer) { JSONSerializer = requireModule("ember-data/serializers/json_serializer")["default"]; }
// container is for lazy transform lookups
var serializer = JSONSerializer.create({ container: this.container });
return serializer.serialize(this, options);
},
/**
Fired when the record is loaded from the server.
@event didLoad
*/
didLoad: Ember.K,
/**
Fired when the record is updated.
@event didUpdate
*/
didUpdate: Ember.K,
/**
Fired when the record is created.
@event didCreate
*/
didCreate: Ember.K,
/**
Fired when the record is deleted.
@event didDelete
*/
didDelete: Ember.K,
/**
Fired when the record becomes invalid.
@event becameInvalid
*/
becameInvalid: Ember.K,
/**
Fired when the record enters the error state.
@event becameError
*/
becameError: Ember.K,
/**
@property data
@private
@type {Object}
*/
data: Ember.computed(function() {
this._data = this._data || {};
return this._data;
}).readOnly(),
_data: null,
init: function() {
this._super();
this._setup();
},
_setup: function() {
this._changesToSync = {};
this._deferredTriggers = [];
this._data = {};
this._attributes = {};
this._inFlightAttributes = {};
this._relationships = {};
/*
implicit relationships are relationship which have not been declared but the inverse side exists on
another record somewhere
For example if there was
```
App.Comment = DS.Model.extend({
name: DS.attr()
})
```
but there is also
```
App.Post = DS.Model.extend({
name: DS.attr(),
comments: DS.hasMany('comment')
})
```
would have a implicit post relationship in order to be do things like remove ourselves from the post
when we are deleted
*/
this._implicitRelationships = Object.create(null);
var model = this;
//TODO Move into a getter for better perf
this.constructor.eachRelationship(function(key, descriptor) {
model._relationships[key] = createRelationshipFor(model, descriptor, model.store);
});
},
/**
@method send
@private
@param {String} name
@param {Object} context
*/
send: function(name, context) {
var currentState = get(this, 'currentState');
if (!currentState[name]) {
this._unhandledEvent(currentState, name, context);
}
return currentState[name](this, context);
},
/**
@method transitionTo
@private
@param {String} name
*/
transitionTo: function(name) {
// POSSIBLE TODO: Remove this code and replace with
// always having direct references to state objects
var pivotName = extractPivotName(name);
var currentState = get(this, 'currentState');
var state = currentState;
do {
if (state.exit) { state.exit(this); }
state = state.parentState;
} while (!state.hasOwnProperty(pivotName));
var path = splitOnDot(name);
var setups = [], enters = [], i, l;
for (i=0, l=path.length; i<l; i++) {
state = state[path[i]];
if (state.enter) { enters.push(state); }
if (state.setup) { setups.push(state); }
}
for (i=0, l=enters.length; i<l; i++) {
enters[i].enter(this);
}
set(this, 'currentState', state);
for (i=0, l=setups.length; i<l; i++) {
setups[i].setup(this);
}
this.updateRecordArraysLater();
},
_unhandledEvent: function(state, name, context) {
var errorMessage = "Attempted to handle event `" + name + "` ";
errorMessage += "on " + String(this) + " while in state ";
errorMessage += state.stateName + ". ";
if (context !== undefined) {
errorMessage += "Called with " + Ember.inspect(context) + ".";
}
throw new Ember.Error(errorMessage);
},
withTransaction: function(fn) {
var transaction = get(this, 'transaction');
if (transaction) { fn(transaction); }
},
/**
@method loadingData
@private
@param {Promise} promise
*/
loadingData: function(promise) {
this.send('loadingData', promise);
},
/**
@method loadedData
@private
*/
loadedData: function() {
this.send('loadedData');
},
/**
@method notFound
@private
*/
notFound: function() {
this.send('notFound');
},
/**
@method pushedData
@private
*/
pushedData: function() {
this.send('pushedData');
},
/**
Marks the record as deleted but does not save it. You must call
`save` afterwards if you want to persist it. You might use this
method if you want to allow the user to still `rollback()` a
delete after it was made.
Example
```javascript
App.ModelDeleteRoute = Ember.Route.extend({
actions: {
softDelete: function() {
this.controller.get('model').deleteRecord();
},
confirm: function() {
this.controller.get('model').save();
},
undo: function() {
this.controller.get('model').rollback();
}
}
});
```
@method deleteRecord
*/
deleteRecord: function() {
this.send('deleteRecord');
},
/**
Same as `deleteRecord`, but saves the record immediately.
Example
```javascript
App.ModelDeleteRoute = Ember.Route.extend({
actions: {
delete: function() {
var controller = this.controller;
controller.get('model').destroyRecord().then(function() {
controller.transitionToRoute('model.index');
});
}
}
});
```
@method destroyRecord
@return {Promise} a promise that will be resolved when the adapter returns
successfully or rejected if the adapter returns with an error.
*/
destroyRecord: function() {
this.deleteRecord();
return this.save();
},
/**
@method unloadRecord
@private
*/
unloadRecord: function() {
if (this.isDestroyed) { return; }
this.send('unloadRecord');
},
/**
@method clearRelationships
@private
*/
clearRelationships: function() {
this.eachRelationship(function(name, relationship) {
var rel = this._relationships[name];
if (rel){
//TODO(Igor) figure out whether we want to clear or disconnect
rel.clear();
rel.destroy();
}
}, this);
},
disconnectRelationships: function() {
this.eachRelationship(function(name, relationship) {
this._relationships[name].disconnect();
}, this);
var model = this;
forEach.call(Ember.keys(this._implicitRelationships), function(key) {
model._implicitRelationships[key].disconnect();
});
},
reconnectRelationships: function() {
this.eachRelationship(function(name, relationship) {
this._relationships[name].reconnect();
}, this);
var model = this;
forEach.call(Ember.keys(this._implicitRelationships), function(key) {
model._implicitRelationships[key].reconnect();
});
},
/**
@method updateRecordArrays
@private
*/
updateRecordArrays: function() {
this._updatingRecordArraysLater = false;
get(this, 'store').dataWasUpdated(this.constructor, this);
},
/**
When a find request is triggered on the store, the user can optionally pass in
attributes and relationships to be preloaded. These are meant to behave as if they
came back from the server, except the user obtained them out of band and is informing
the store of their existence. The most common use case is for supporting client side
nested URLs, such as `/posts/1/comments/2` so the user can do
`store.find('comment', 2, {post:1})` without having to fetch the post.
Preloaded data can be attributes and relationships passed in either as IDs or as actual
models.
@method _preloadData
@private
@param {Object} preload
*/
_preloadData: function(preload) {
var record = this;
//TODO(Igor) consider the polymorphic case
forEach.call(Ember.keys(preload), function(key) {
var preloadValue = get(preload, key);
var relationshipMeta = record.constructor.metaForProperty(key);
if (relationshipMeta.isRelationship) {
record._preloadRelationship(key, preloadValue);
} else {
get(record, '_data')[key] = preloadValue;
}
});
},
_preloadRelationship: function(key, preloadValue) {
var relationshipMeta = this.constructor.metaForProperty(key);
var type = relationshipMeta.type;
if (relationshipMeta.kind === 'hasMany'){
this._preloadHasMany(key, preloadValue, type);
} else {
this._preloadBelongsTo(key, preloadValue, type);
}
},
_preloadHasMany: function(key, preloadValue, type) {
Ember.assert("You need to pass in an array to set a hasMany property on a record", Ember.isArray(preloadValue));
var record = this;
var recordsToSet = map.call(preloadValue, function(recordToPush) {
return record._convertStringOrNumberIntoRecord(recordToPush, type);
});
//We use the pathway of setting the hasMany as if it came from the adapter
//because the user told us that they know this relationships exists already
this._relationships[key].updateRecordsFromAdapter(recordsToSet);
},
_preloadBelongsTo: function(key, preloadValue, type){
var recordToSet = this._convertStringOrNumberIntoRecord(preloadValue, type);
//We use the pathway of setting the hasMany as if it came from the adapter
//because the user told us that they know this relationships exists already
this._relationships[key].setRecord(recordToSet);
},
_convertStringOrNumberIntoRecord: function(value, type) {
if (Ember.typeOf(value) === 'string' || Ember.typeOf(value) === 'number'){
return this.store.recordForId(type, value);
}
return value;
},
/**
Returns an object, whose keys are changed properties, and value is
an [oldProp, newProp] array.
Example
```javascript
App.Mascot = DS.Model.extend({
name: attr('string')
});
var person = store.createRecord('person');
person.changedAttributes(); // {}
person.set('name', 'Tomster');
person.changedAttributes(); // {name: [undefined, 'Tomster']}
```
@method changedAttributes
@return {Object} an object, whose keys are changed properties,
and value is an [oldProp, newProp] array.
*/
changedAttributes: function() {
var oldData = get(this, '_data');
var newData = get(this, '_attributes');
var diffData = {};
var prop;
for (prop in newData) {
diffData[prop] = [oldData[prop], newData[prop]];
}
return diffData;
},
/**
@method adapterWillCommit
@private
*/
adapterWillCommit: function() {
this.send('willCommit');
},
/**
If the adapter did not return a hash in response to a commit,
merge the changed attributes and relationships into the existing
saved data.
@method adapterDidCommit
*/
adapterDidCommit: function(data) {
set(this, 'isError', false);
if (data) {
this._data = data;
} else {
Ember.mixin(this._data, this._inFlightAttributes);
}
this._inFlightAttributes = {};
this.send('didCommit');
this.updateRecordArraysLater();
if (!data) { return; }
this.notifyPropertyChange('data');
},
/**
@method adapterDidDirty
@private
*/
adapterDidDirty: function() {
this.send('becomeDirty');
this.updateRecordArraysLater();
},
/**
@method updateRecordArraysLater
@private
*/
updateRecordArraysLater: function() {
// quick hack (something like this could be pushed into run.once
if (this._updatingRecordArraysLater) { return; }
this._updatingRecordArraysLater = true;
Ember.run.schedule('actions', this, this.updateRecordArrays);
},
/**
@method setupData
@private
@param {Object} data
@param {Boolean} partial the data should be merged into
the existing data, not replace it.
*/
setupData: function(data, partial) {
if (partial) {
Ember.merge(this._data, data);
} else {
this._data = data;
}
if (data) { this.pushedData(); }
this.notifyPropertyChange('data');
},
materializeId: function(id) {
set(this, 'id', id);
},
materializeAttributes: function(attributes) {
Ember.assert("Must pass a hash of attributes to materializeAttributes", !!attributes);
merge(this._data, attributes);
},
materializeAttribute: function(name, value) {
this._data[name] = value;
},
/**
If the model `isDirty` this function will discard any unsaved
changes
Example
```javascript
record.get('name'); // 'Untitled Document'
record.set('name', 'Doc 1');
record.get('name'); // 'Doc 1'
record.rollback();
record.get('name'); // 'Untitled Document'
```
@method rollback
*/
rollback: function() {
this._attributes = {};
if (get(this, 'isError')) {
this._inFlightAttributes = {};
set(this, 'isError', false);
}
//Eventually rollback will always work for relationships
//For now we support it only out of deleted state, because we
//have an explicit way of knowing when the server acked the relationship change
if (get(this, 'isDeleted')) {
this.reconnectRelationships();
}
if (!get(this, 'isValid')) {
this._inFlightAttributes = {};
}
this.send('rolledBack');
this.notifyPropertyChange('data');
},
toStringExtension: function() {
return get(this, 'id');
},
/**
Save the record and persist any changes to the record to an
external source via the adapter.
Example
```javascript
record.set('name', 'Tomster');
record.save().then(function(){
// Success callback
}, function() {
// Error callback
});
```
@method save
@return {Promise} a promise that will be resolved when the adapter returns
successfully or rejected if the adapter returns with an error.
*/
save: function() {
var promiseLabel = "DS: Model#save " + this;
var resolver = Ember.RSVP.defer(promiseLabel);
this.get('store').scheduleSave(this, resolver);
this._inFlightAttributes = this._attributes;
this._attributes = {};
return PromiseObject.create({
promise: resolver.promise
});
},
/**
Reload the record from the adapter.
This will only work if the record has already finished loading
and has not yet been modified (`isLoaded` but not `isDirty`,
or `isSaving`).
Example
```javascript
App.ModelViewRoute = Ember.Route.extend({
actions: {
reload: function() {
this.controller.get('model').reload();
}
}
});
```
@method reload
@return {Promise} a promise that will be resolved with the record when the
adapter returns successfully or rejected if the adapter returns
with an error.
*/
reload: function() {
set(this, 'isReloading', true);
var record = this;
var promiseLabel = "DS: Model#reload of " + this;
var promise = new Promise(function(resolve){
record.send('reloadRecord', resolve);
}, promiseLabel).then(function() {
record.set('isReloading', false);
record.set('isError', false);
return record;
}, function(reason) {
record.set('isError', true);
throw reason;
}, "DS: Model#reload complete, update flags")['finally'](function () {
record.updateRecordArrays();
});
return PromiseObject.create({
promise: promise
});
},
// FOR USE DURING COMMIT PROCESS
adapterDidUpdateAttribute: function(attributeName, value) {
// If a value is passed in, update the internal attributes and clear
// the attribute cache so it picks up the new value. Otherwise,
// collapse the current value into the internal attributes because
// the adapter has acknowledged it.
if (value !== undefined) {
this._data[attributeName] = value;
this.notifyPropertyChange(attributeName);
} else {
this._data[attributeName] = this._inFlightAttributes[attributeName];
}
this.updateRecordArraysLater();
},
/**
@method adapterDidInvalidate
@private
*/
adapterDidInvalidate: function(errors) {
var recordErrors = get(this, 'errors');
function addError(name) {
if (errors[name]) {
recordErrors.add(name, errors[name]);
}
}
this.eachAttribute(addError);
this.eachRelationship(addError);
},
/**
@method adapterDidError
@private
*/
adapterDidError: function() {
this.send('becameError');
set(this, 'isError', true);
},
/**
Override the default event firing from Ember.Evented to
also call methods with the given name.
@method trigger
@private
@param {String} name
*/
trigger: function() {
var length = arguments.length;
var args = new Array(length - 1);
var name = arguments[0];
for (var i = 1; i < length; i++ ){
args[i - 1] = arguments[i];
}
Ember.tryInvoke(this, name, args);
this._super.apply(this, arguments);
},
triggerLater: function() {
var length = arguments.length;
var args = new Array(length);
for (var i = 0; i < length; i++ ){
args[i] = arguments[i];
}
if (this._deferredTriggers.push(args) !== 1) {
return;
}
Ember.run.schedule('actions', this, '_triggerDeferredTriggers');
},
_triggerDeferredTriggers: function() {
for (var i=0, l= this._deferredTriggers.length; i<l; i++) {
this.trigger.apply(this, this._deferredTriggers[i]);
}
this._deferredTriggers.length = 0;
},
willDestroy: function() {
this._super();
this.clearRelationships();
},
// This is a temporary solution until we refactor DS.Model to not
// rely on the data property.
willMergeMixin: function(props) {
Ember.assert('`data` is a reserved property name on DS.Model objects. Please choose a different property name for ' + this.constructor.toString(), !props.data);
}
});
Model.reopenClass({
/**
Alias DS.Model's `create` method to `_create`. This allows us to create DS.Model
instances from within the store, but if end users accidentally call `create()`
(instead of `createRecord()`), we can raise an error.
@method _create
@private
@static
*/
_create: Model.create,
/**
Override the class' `create()` method to raise an error. This
prevents end users from inadvertently calling `create()` instead
of `createRecord()`. The store is still able to create instances
by calling the `_create()` method. To create an instance of a
`DS.Model` use [store.createRecord](DS.Store.html#method_createRecord).
@method create
@private
@static
*/
create: function() {
throw new Ember.Error("You should not call `create` on a model. Instead, call `store.createRecord` with the attributes you would like to set.");
}
});
__exports__["default"] = Model;
});
define("ember-data/system/model/states",
["exports"],
function(__exports__) {
"use strict";
/**
@module ember-data
*/
var get = Ember.get;
var set = Ember.set;
/*
This file encapsulates the various states that a record can transition
through during its lifecycle.
*/
/**
### State
Each record has a `currentState` property that explicitly tracks what
state a record is in at any given time. For instance, if a record is
newly created and has not yet been sent to the adapter to be saved,
it would be in the `root.loaded.created.uncommitted` state. If a
record has had local modifications made to it that are in the
process of being saved, the record would be in the
`root.loaded.updated.inFlight` state. (This state paths will be
explained in more detail below.)
Events are sent by the record or its store to the record's
`currentState` property. How the state reacts to these events is
dependent on which state it is in. In some states, certain events
will be invalid and will cause an exception to be raised.
States are hierarchical and every state is a substate of the
`RootState`. For example, a record can be in the
`root.deleted.uncommitted` state, then transition into the
`root.deleted.inFlight` state. If a child state does not implement
an event handler, the state manager will attempt to invoke the event
on all parent states until the root state is reached. The state
hierarchy of a record is described in terms of a path string. You
can determine a record's current state by getting the state's
`stateName` property:
```javascript
record.get('currentState.stateName');
//=> "root.created.uncommitted"
```
The hierarchy of valid states that ship with ember data looks like
this:
```text
* root
* deleted
* saved
* uncommitted
* inFlight
* empty
* loaded
* created
* uncommitted
* inFlight
* saved
* updated
* uncommitted
* inFlight
* loading
```
The `DS.Model` states are themselves stateless. What that means is
that, the hierarchical states that each of *those* points to is a
shared data structure. For performance reasons, instead of each
record getting its own copy of the hierarchy of states, each record
points to this global, immutable shared instance. How does a state
know which record it should be acting on? We pass the record
instance into the state's event handlers as the first argument.
The record passed as the first parameter is where you should stash
state about the record if needed; you should never store data on the state
object itself.
### Events and Flags
A state may implement zero or more events and flags.
#### Events
Events are named functions that are invoked when sent to a record. The
record will first look for a method with the given name on the
current state. If no method is found, it will search the current
state's parent, and then its grandparent, and so on until reaching
the top of the hierarchy. If the root is reached without an event
handler being found, an exception will be raised. This can be very
helpful when debugging new features.
Here's an example implementation of a state with a `myEvent` event handler:
```javascript
aState: DS.State.create({
myEvent: function(manager, param) {
console.log("Received myEvent with", param);
}
})
```
To trigger this event:
```javascript
record.send('myEvent', 'foo');
//=> "Received myEvent with foo"
```
Note that an optional parameter can be sent to a record's `send()` method,
which will be passed as the second parameter to the event handler.
Events should transition to a different state if appropriate. This can be
done by calling the record's `transitionTo()` method with a path to the
desired state. The state manager will attempt to resolve the state path
relative to the current state. If no state is found at that path, it will
attempt to resolve it relative to the current state's parent, and then its
parent, and so on until the root is reached. For example, imagine a hierarchy
like this:
* created
* uncommitted <-- currentState
* inFlight
* updated
* inFlight
If we are currently in the `uncommitted` state, calling
`transitionTo('inFlight')` would transition to the `created.inFlight` state,
while calling `transitionTo('updated.inFlight')` would transition to
the `updated.inFlight` state.
Remember that *only events* should ever cause a state transition. You should
never call `transitionTo()` from outside a state's event handler. If you are
tempted to do so, create a new event and send that to the state manager.
#### Flags
Flags are Boolean values that can be used to introspect a record's current
state in a more user-friendly way than examining its state path. For example,
instead of doing this:
```javascript
var statePath = record.get('stateManager.currentPath');
if (statePath === 'created.inFlight') {
doSomething();
}
```
You can say:
```javascript
if (record.get('isNew') && record.get('isSaving')) {
doSomething();
}
```
If your state does not set a value for a given flag, the value will
be inherited from its parent (or the first place in the state hierarchy
where it is defined).
The current set of flags are defined below. If you want to add a new flag,
in addition to the area below, you will also need to declare it in the
`DS.Model` class.
* [isEmpty](DS.Model.html#property_isEmpty)
* [isLoading](DS.Model.html#property_isLoading)
* [isLoaded](DS.Model.html#property_isLoaded)
* [isDirty](DS.Model.html#property_isDirty)
* [isSaving](DS.Model.html#property_isSaving)
* [isDeleted](DS.Model.html#property_isDeleted)
* [isNew](DS.Model.html#property_isNew)
* [isValid](DS.Model.html#property_isValid)
@namespace DS
@class RootState
*/
function didSetProperty(record, context) {
if (context.value === context.originalValue) {
delete record._attributes[context.name];
record.send('propertyWasReset', context.name);
} else if (context.value !== context.oldValue) {
record.send('becomeDirty');
}
record.updateRecordArraysLater();
}
// Implementation notes:
//
// Each state has a boolean value for all of the following flags:
//
// * isLoaded: The record has a populated `data` property. When a
// record is loaded via `store.find`, `isLoaded` is false
// until the adapter sets it. When a record is created locally,
// its `isLoaded` property is always true.
// * isDirty: The record has local changes that have not yet been
// saved by the adapter. This includes records that have been
// created (but not yet saved) or deleted.
// * isSaving: The record has been committed, but
// the adapter has not yet acknowledged that the changes have
// been persisted to the backend.
// * isDeleted: The record was marked for deletion. When `isDeleted`
// is true and `isDirty` is true, the record is deleted locally
// but the deletion was not yet persisted. When `isSaving` is
// true, the change is in-flight. When both `isDirty` and
// `isSaving` are false, the change has persisted.
// * isError: The adapter reported that it was unable to save
// local changes to the backend. This may also result in the
// record having its `isValid` property become false if the
// adapter reported that server-side validations failed.
// * isNew: The record was created on the client and the adapter
// did not yet report that it was successfully saved.
// * isValid: The adapter did not report any server-side validation
// failures.
// The dirty state is a abstract state whose functionality is
// shared between the `created` and `updated` states.
//
// The deleted state shares the `isDirty` flag with the
// subclasses of `DirtyState`, but with a very different
// implementation.
//
// Dirty states have three child states:
//
// `uncommitted`: the store has not yet handed off the record
// to be saved.
// `inFlight`: the store has handed off the record to be saved,
// but the adapter has not yet acknowledged success.
// `invalid`: the record has invalid information and cannot be
// send to the adapter yet.
var DirtyState = {
initialState: 'uncommitted',
// FLAGS
isDirty: true,
// SUBSTATES
// When a record first becomes dirty, it is `uncommitted`.
// This means that there are local pending changes, but they
// have not yet begun to be saved, and are not invalid.
uncommitted: {
// EVENTS
didSetProperty: didSetProperty,
//TODO(Igor) reloading now triggers a
//loadingData event, though it seems fine?
loadingData: Ember.K,
propertyWasReset: function(record, name) {
var length = Ember.keys(record._attributes);
var stillDirty = length > 0;
if (!stillDirty) { record.send('rolledBack'); }
},
pushedData: Ember.K,
becomeDirty: Ember.K,
willCommit: function(record) {
record.transitionTo('inFlight');
},
reloadRecord: function(record, resolve) {
resolve(get(record, 'store').reloadRecord(record));
},
rolledBack: function(record) {
record.transitionTo('loaded.saved');
},
becameInvalid: function(record) {
record.transitionTo('invalid');
},
rollback: function(record) {
record.rollback();
}
},
// Once a record has been handed off to the adapter to be
// saved, it is in the 'in flight' state. Changes to the
// record cannot be made during this window.
inFlight: {
// FLAGS
isSaving: true,
// EVENTS
didSetProperty: didSetProperty,
becomeDirty: Ember.K,
pushedData: Ember.K,
unloadRecord: function(record) {
Ember.assert("You can only unload a record which is not inFlight. `" + Ember.inspect(record) + " `", false);
},
// TODO: More robust semantics around save-while-in-flight
willCommit: Ember.K,
didCommit: function(record) {
var dirtyType = get(this, 'dirtyType');
record.transitionTo('saved');
record.send('invokeLifecycleCallbacks', dirtyType);
},
becameInvalid: function(record) {
record.transitionTo('invalid');
record.send('invokeLifecycleCallbacks');
},
becameError: function(record) {
record.transitionTo('uncommitted');
record.triggerLater('becameError', record);
}
},
// A record is in the `invalid` if the adapter has indicated
// the the record failed server-side invalidations.
invalid: {
// FLAGS
isValid: false,
// EVENTS
deleteRecord: function(record) {
record.transitionTo('deleted.uncommitted');
record.disconnectRelationships();
},
didSetProperty: function(record, context) {
get(record, 'errors').remove(context.name);
didSetProperty(record, context);
},
becomeDirty: Ember.K,
willCommit: function(record) {
get(record, 'errors').clear();
record.transitionTo('inFlight');
},
rolledBack: function(record) {
get(record, 'errors').clear();
},
becameValid: function(record) {
record.transitionTo('uncommitted');
},
invokeLifecycleCallbacks: function(record) {
record.triggerLater('becameInvalid', record);
},
exit: function(record) {
record._inFlightAttributes = {};
}
}
};
// The created and updated states are created outside the state
// chart so we can reopen their substates and add mixins as
// necessary.
function deepClone(object) {
var clone = {}, value;
for (var prop in object) {
value = object[prop];
if (value && typeof value === 'object') {
clone[prop] = deepClone(value);
} else {
clone[prop] = value;
}
}
return clone;
}
function mixin(original, hash) {
for (var prop in hash) {
original[prop] = hash[prop];
}
return original;
}
function dirtyState(options) {
var newState = deepClone(DirtyState);
return mixin(newState, options);
}
var createdState = dirtyState({
dirtyType: 'created',
// FLAGS
isNew: true
});
createdState.uncommitted.rolledBack = function(record) {
record.transitionTo('deleted.saved');
};
var updatedState = dirtyState({
dirtyType: 'updated'
});
createdState.uncommitted.deleteRecord = function(record) {
record.disconnectRelationships();
record.transitionTo('deleted.saved');
};
createdState.uncommitted.rollback = function(record) {
DirtyState.uncommitted.rollback.apply(this, arguments);
record.transitionTo('deleted.saved');
};
createdState.uncommitted.propertyWasReset = Ember.K;
function assertAgainstUnloadRecord(record) {
Ember.assert("You can only unload a record which is not inFlight. `" + Ember.inspect(record) + "`", false);
}
updatedState.inFlight.unloadRecord = assertAgainstUnloadRecord;
updatedState.uncommitted.deleteRecord = function(record) {
record.transitionTo('deleted.uncommitted');
record.disconnectRelationships();
};
var RootState = {
// FLAGS
isEmpty: false,
isLoading: false,
isLoaded: false,
isDirty: false,
isSaving: false,
isDeleted: false,
isNew: false,
isValid: true,
// DEFAULT EVENTS
// Trying to roll back if you're not in the dirty state
// doesn't change your state. For example, if you're in the
// in-flight state, rolling back the record doesn't move
// you out of the in-flight state.
rolledBack: Ember.K,
unloadRecord: function(record) {
// clear relationships before moving to deleted state
// otherwise it fails
record.clearRelationships();
record.transitionTo('deleted.saved');
},
propertyWasReset: Ember.K,
// SUBSTATES
// A record begins its lifecycle in the `empty` state.
// If its data will come from the adapter, it will
// transition into the `loading` state. Otherwise, if
// the record is being created on the client, it will
// transition into the `created` state.
empty: {
isEmpty: true,
// EVENTS
loadingData: function(record, promise) {
record._loadingPromise = promise;
record.transitionTo('loading');
},
loadedData: function(record) {
record.transitionTo('loaded.created.uncommitted');
record.notifyPropertyChange('data');
},
pushedData: function(record) {
record.transitionTo('loaded.saved');
record.triggerLater('didLoad');
}
},
// A record enters this state when the store asks
// the adapter for its data. It remains in this state
// until the adapter provides the requested data.
//
// Usually, this process is asynchronous, using an
// XHR to retrieve the data.
loading: {
// FLAGS
isLoading: true,
exit: function(record) {
record._loadingPromise = null;
},
// EVENTS
pushedData: function(record) {
record.transitionTo('loaded.saved');
record.triggerLater('didLoad');
set(record, 'isError', false);
},
becameError: function(record) {
record.triggerLater('becameError', record);
},
notFound: function(record) {
record.transitionTo('empty');
}
},
// A record enters this state when its data is populated.
// Most of a record's lifecycle is spent inside substates
// of the `loaded` state.
loaded: {
initialState: 'saved',
// FLAGS
isLoaded: true,
//TODO(Igor) Reloading now triggers a loadingData event,
//but it should be ok?
loadingData: Ember.K,
// SUBSTATES
// If there are no local changes to a record, it remains
// in the `saved` state.
saved: {
setup: function(record) {
var attrs = record._attributes;
var isDirty = false;
for (var prop in attrs) {
if (attrs.hasOwnProperty(prop)) {
isDirty = true;
break;
}
}
if (isDirty) {
record.adapterDidDirty();
}
},
// EVENTS
didSetProperty: didSetProperty,
pushedData: Ember.K,
becomeDirty: function(record) {
record.transitionTo('updated.uncommitted');
},
willCommit: function(record) {
record.transitionTo('updated.inFlight');
},
reloadRecord: function(record, resolve) {
resolve(get(record, 'store').reloadRecord(record));
},
deleteRecord: function(record) {
record.transitionTo('deleted.uncommitted');
record.disconnectRelationships();
},
unloadRecord: function(record) {
// clear relationships before moving to deleted state
// otherwise it fails
record.clearRelationships();
record.transitionTo('deleted.saved');
},
didCommit: function(record) {
record.send('invokeLifecycleCallbacks', get(record, 'lastDirtyType'));
},
// loaded.saved.notFound would be triggered by a failed
// `reload()` on an unchanged record
notFound: Ember.K
},
// A record is in this state after it has been locally
// created but before the adapter has indicated that
// it has been saved.
created: createdState,
// A record is in this state if it has already been
// saved to the server, but there are new local changes
// that have not yet been saved.
updated: updatedState
},
// A record is in this state if it was deleted from the store.
deleted: {
initialState: 'uncommitted',
dirtyType: 'deleted',
// FLAGS
isDeleted: true,
isLoaded: true,
isDirty: true,
// TRANSITIONS
setup: function(record) {
record.updateRecordArrays();
},
// SUBSTATES
// When a record is deleted, it enters the `start`
// state. It will exit this state when the record
// starts to commit.
uncommitted: {
// EVENTS
willCommit: function(record) {
record.transitionTo('inFlight');
},
rollback: function(record) {
record.rollback();
},
becomeDirty: Ember.K,
deleteRecord: Ember.K,
rolledBack: function(record) {
record.transitionTo('loaded.saved');
}
},
// After a record starts committing, but
// before the adapter indicates that the deletion
// has saved to the server, a record is in the
// `inFlight` substate of `deleted`.
inFlight: {
// FLAGS
isSaving: true,
// EVENTS
unloadRecord: assertAgainstUnloadRecord,
// TODO: More robust semantics around save-while-in-flight
willCommit: Ember.K,
didCommit: function(record) {
record.transitionTo('saved');
record.send('invokeLifecycleCallbacks');
},
becameError: function(record) {
record.transitionTo('uncommitted');
record.triggerLater('becameError', record);
}
},
// Once the adapter indicates that the deletion has
// been saved, the record enters the `saved` substate
// of `deleted`.
saved: {
// FLAGS
isDirty: false,
setup: function(record) {
var store = get(record, 'store');
store.dematerializeRecord(record);
},
invokeLifecycleCallbacks: function(record) {
record.triggerLater('didDelete', record);
record.triggerLater('didCommit', record);
},
willCommit: Ember.K,
didCommit: Ember.K
}
},
invokeLifecycleCallbacks: function(record, dirtyType) {
if (dirtyType === 'created') {
record.triggerLater('didCreate', record);
} else {
record.triggerLater('didUpdate', record);
}
record.triggerLater('didCommit', record);
}
};
function wireState(object, parent, name) {
/*jshint proto:true*/
// TODO: Use Object.create and copy instead
object = mixin(parent ? Ember.create(parent) : {}, object);
object.parentState = parent;
object.stateName = name;
for (var prop in object) {
if (!object.hasOwnProperty(prop) || prop === 'parentState' || prop === 'stateName') { continue; }
if (typeof object[prop] === 'object') {
object[prop] = wireState(object[prop], object, name + "." + prop);
}
}
return object;
}
RootState = wireState(RootState, null, "root");
__exports__["default"] = RootState;
});
define("ember-data/system/promise_proxies",
["exports"],
function(__exports__) {
"use strict";
var Promise = Ember.RSVP.Promise;
var get = Ember.get;
/**
A `PromiseArray` is an object that acts like both an `Ember.Array`
and a promise. When the promise is resolved the resulting value
will be set to the `PromiseArray`'s `content` property. This makes
it easy to create data bindings with the `PromiseArray` that will be
updated when the promise resolves.
For more information see the [Ember.PromiseProxyMixin
documentation](/api/classes/Ember.PromiseProxyMixin.html).
Example
```javascript
var promiseArray = DS.PromiseArray.create({
promise: $.getJSON('/some/remote/data.json')
});
promiseArray.get('length'); // 0
promiseArray.then(function() {
promiseArray.get('length'); // 100
});
```
@class PromiseArray
@namespace DS
@extends Ember.ArrayProxy
@uses Ember.PromiseProxyMixin
*/
var PromiseArray = Ember.ArrayProxy.extend(Ember.PromiseProxyMixin);
/**
A `PromiseObject` is an object that acts like both an `Ember.Object`
and a promise. When the promise is resolved, then the resulting value
will be set to the `PromiseObject`'s `content` property. This makes
it easy to create data bindings with the `PromiseObject` that will
be updated when the promise resolves.
For more information see the [Ember.PromiseProxyMixin
documentation](/api/classes/Ember.PromiseProxyMixin.html).
Example
```javascript
var promiseObject = DS.PromiseObject.create({
promise: $.getJSON('/some/remote/data.json')
});
promiseObject.get('name'); // null
promiseObject.then(function() {
promiseObject.get('name'); // 'Tomster'
});
```
@class PromiseObject
@namespace DS
@extends Ember.ObjectProxy
@uses Ember.PromiseProxyMixin
*/
var PromiseObject = Ember.ObjectProxy.extend(Ember.PromiseProxyMixin);
var promiseObject = function(promise, label) {
return PromiseObject.create({
promise: Promise.resolve(promise, label)
});
};
var promiseArray = function(promise, label) {
return PromiseArray.create({
promise: Promise.resolve(promise, label)
});
};
/**
A PromiseManyArray is a PromiseArray that also proxies certain method calls
to the underlying manyArray.
Right now we proxy:
`reload()`
*/
var PromiseManyArray = PromiseArray.extend({
reload: function() {
//I don't think this should ever happen right now, but worth guarding if we refactor the async relationships
Ember.assert('You are trying to reload an async manyArray before it has been created', get(this, 'content'));
return get(this, 'content').reload();
}
});
var promiseManyArray = function(promise, label) {
return PromiseManyArray.create({
promise: Promise.resolve(promise, label)
});
};
__exports__.PromiseArray = PromiseArray;
__exports__.PromiseObject = PromiseObject;
__exports__.PromiseManyArray = PromiseManyArray;
__exports__.promiseArray = promiseArray;
__exports__.promiseObject = promiseObject;
__exports__.promiseManyArray = promiseManyArray;
});
define("ember-data/system/record_array_manager",
["ember-data/system/record_arrays","ember-data/system/map","exports"],
function(__dependency1__, __dependency2__, __exports__) {
"use strict";
/**
@module ember-data
*/
var RecordArray = __dependency1__.RecordArray;
var FilteredRecordArray = __dependency1__.FilteredRecordArray;
var AdapterPopulatedRecordArray = __dependency1__.AdapterPopulatedRecordArray;
var ManyArray = __dependency1__.ManyArray;
var MapWithDefault = __dependency2__.MapWithDefault;
var OrderedSet = __dependency2__.OrderedSet;
var get = Ember.get;
var forEach = Ember.EnumerableUtils.forEach;
/**
@class RecordArrayManager
@namespace DS
@private
@extends Ember.Object
*/
__exports__["default"] = Ember.Object.extend({
init: function() {
this.filteredRecordArrays = MapWithDefault.create({
defaultValue: function() { return []; }
});
this.changedRecords = [];
this._adapterPopulatedRecordArrays = [];
},
recordDidChange: function(record) {
if (this.changedRecords.push(record) !== 1) { return; }
Ember.run.schedule('actions', this, this.updateRecordArrays);
},
recordArraysForRecord: function(record) {
record._recordArrays = record._recordArrays || OrderedSet.create();
return record._recordArrays;
},
/**
This method is invoked whenever data is loaded into the store by the
adapter or updated by the adapter, or when a record has changed.
It updates all record arrays that a record belongs to.
To avoid thrashing, it only runs at most once per run loop.
@method updateRecordArrays
@param {Class} type
@param {Number|String} clientId
*/
updateRecordArrays: function() {
forEach(this.changedRecords, function(record) {
if (get(record, 'isDeleted')) {
this._recordWasDeleted(record);
} else {
this._recordWasChanged(record);
}
}, this);
this.changedRecords.length = 0;
},
_recordWasDeleted: function (record) {
var recordArrays = record._recordArrays;
if (!recordArrays) { return; }
recordArrays.forEach(function(array){
array.removeRecord(record);
});
record._recordArrays = null;
},
_recordWasChanged: function (record) {
var type = record.constructor;
var recordArrays = this.filteredRecordArrays.get(type);
var filter;
forEach(recordArrays, function(array) {
filter = get(array, 'filterFunction');
this.updateRecordArray(array, filter, type, record);
}, this);
// loop through all manyArrays containing an unloaded copy of this
// clientId and notify them that the record was loaded.
var manyArrays = record._loadingRecordArrays;
if (manyArrays) {
for (var i=0, l=manyArrays.length; i<l; i++) {
manyArrays[i].loadedRecord();
}
record._loadingRecordArrays = [];
}
},
/**
Update an individual filter.
@method updateRecordArray
@param {DS.FilteredRecordArray} array
@param {Function} filter
@param {Class} type
@param {Number|String} clientId
*/
updateRecordArray: function(array, filter, type, record) {
var shouldBeInArray;
if (!filter) {
shouldBeInArray = true;
} else {
shouldBeInArray = filter(record);
}
var recordArrays = this.recordArraysForRecord(record);
if (shouldBeInArray) {
if (!recordArrays.has(array)) {
array.pushRecord(record);
recordArrays.add(array);
}
} else if (!shouldBeInArray) {
recordArrays["delete"](array);
array.removeRecord(record);
}
},
/**
This method is invoked if the `filterFunction` property is
changed on a `DS.FilteredRecordArray`.
It essentially re-runs the filter from scratch. This same
method is invoked when the filter is created in th first place.
@method updateFilter
@param {Array} array
@param {String} type
@param {Function} filter
*/
updateFilter: function(array, type, filter) {
var typeMap = this.store.typeMapFor(type);
var records = typeMap.records, record;
for (var i=0, l=records.length; i<l; i++) {
record = records[i];
if (!get(record, 'isDeleted') && !get(record, 'isEmpty')) {
this.updateRecordArray(array, filter, type, record);
}
}
},
/**
Create a `DS.ManyArray` for a type and list of record references, and index
the `ManyArray` under each reference.  This will cause the array to update
automatically when records of that type change attribute
values or states.
@method registerFilteredRecordArray
@param {DS.RecordArray} array
@param {Class} type
@param {Function} filter
*/
registerFilteredRecordArray: function(array, type, filter) {
var recordArrays = this.filteredRecordArrays.get(type);
recordArrays.push(array);
this.updateFilter(array, type, filter);
},
// Internally, we maintain a map of all unloaded IDs requested by
// a ManyArray. As the adapter loads data into the store, the
// store notifies any interested ManyArrays. When the ManyArray's
// total number of loading records drops to zero, it becomes
// `isLoaded` and fires a `didLoad` event.
registerWaitingRecordArray: function(record, array) {
var loadingRecordArrays = record._loadingRecordArrays || [];
loadingRecordArrays.push(array);
record._loadingRecordArrays = loadingRecordArrays;
},
willDestroy: function(){
this._super();
forEach(flatten(values(this.filteredRecordArrays.values)), destroy);
forEach(this._adapterPopulatedRecordArrays, destroy);
}
});
function values(obj) {
var result = [];
var keys = Ember.keys(obj);
for (var i = 0; i < keys.length; i++) {
result.push(obj[keys[i]]);
}
return result;
}
function destroy(entry) {
entry.destroy();
}
function flatten(list) {
var length = list.length;
var result = Ember.A();
for (var i = 0; i < length; i++) {
result = result.concat(list[i]);
}
return result;
}
});
define("ember-data/system/record_arrays",
["ember-data/system/record_arrays/record_array","ember-data/system/record_arrays/filtered_record_array","ember-data/system/record_arrays/adapter_populated_record_array","ember-data/system/record_arrays/many_array","exports"],
function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __exports__) {
"use strict";
/**
@module ember-data
*/
var RecordArray = __dependency1__["default"];
var FilteredRecordArray = __dependency2__["default"];
var AdapterPopulatedRecordArray = __dependency3__["default"];
var ManyArray = __dependency4__["default"];
__exports__.RecordArray = RecordArray;
__exports__.FilteredRecordArray = FilteredRecordArray;
__exports__.AdapterPopulatedRecordArray = AdapterPopulatedRecordArray;
__exports__.ManyArray = ManyArray;
});
define("ember-data/system/record_arrays/adapter_populated_record_array",
["ember-data/system/record_arrays/record_array","exports"],
function(__dependency1__, __exports__) {
"use strict";
var RecordArray = __dependency1__["default"];
/**
@module ember-data
*/
var get = Ember.get;
function cloneNull(source) {
var clone = Object.create(null);
for (var key in source) {
clone[key] = source[key];
}
return clone;
}
/**
Represents an ordered list of records whose order and membership is
determined by the adapter. For example, a query sent to the adapter
may trigger a search on the server, whose results would be loaded
into an instance of the `AdapterPopulatedRecordArray`.
@class AdapterPopulatedRecordArray
@namespace DS
@extends DS.RecordArray
*/
__exports__["default"] = RecordArray.extend({
query: null,
replace: function() {
var type = get(this, 'type').toString();
throw new Error("The result of a server query (on " + type + ") is immutable.");
},
/**
@method load
@private
@param {Array} data
*/
load: function(data) {
var store = get(this, 'store');
var type = get(this, 'type');
var records = store.pushMany(type, data);
var meta = store.metadataFor(type);
this.setProperties({
content: Ember.A(records),
isLoaded: true,
meta: cloneNull(meta)
});
records.forEach(function(record) {
this.manager.recordArraysForRecord(record).add(this);
}, this);
// TODO: should triggering didLoad event be the last action of the runLoop?
Ember.run.once(this, 'trigger', 'didLoad');
}
});
});
define("ember-data/system/record_arrays/filtered_record_array",
["ember-data/system/record_arrays/record_array","exports"],
function(__dependency1__, __exports__) {
"use strict";
var RecordArray = __dependency1__["default"];
/**
@module ember-data
*/
var get = Ember.get;
/**
Represents a list of records whose membership is determined by the
store. As records are created, loaded, or modified, the store
evaluates them to determine if they should be part of the record
array.
@class FilteredRecordArray
@namespace DS
@extends DS.RecordArray
*/
__exports__["default"] = RecordArray.extend({
/**
The filterFunction is a function used to test records from the store to
determine if they should be part of the record array.
Example
```javascript
var allPeople = store.all('person');
allPeople.mapBy('name'); // ["Tom Dale", "Yehuda Katz", "Trek Glowacki"]
var people = store.filter('person', function(person) {
if (person.get('name').match(/Katz$/)) { return true; }
});
people.mapBy('name'); // ["Yehuda Katz"]
var notKatzFilter = function(person) {
return !person.get('name').match(/Katz$/);
};
people.set('filterFunction', notKatzFilter);
people.mapBy('name'); // ["Tom Dale", "Trek Glowacki"]
```
@method filterFunction
@param {DS.Model} record
@return {Boolean} `true` if the record should be in the array
*/
filterFunction: null,
isLoaded: true,
replace: function() {
var type = get(this, 'type').toString();
throw new Error("The result of a client-side filter (on " + type + ") is immutable.");
},
/**
@method updateFilter
@private
*/
_updateFilter: function() {
var manager = get(this, 'manager');
manager.updateFilter(this, get(this, 'type'), get(this, 'filterFunction'));
},
updateFilter: Ember.observer(function() {
Ember.run.once(this, this._updateFilter);
}, 'filterFunction')
});
});
define("ember-data/system/record_arrays/many_array",
["ember-data/system/record_arrays/record_array","exports"],
function(__dependency1__, __exports__) {
"use strict";
var RecordArray = __dependency1__["default"];
/**
@module ember-data
*/
var get = Ember.get, set = Ember.set;
/**
A `ManyArray` is a `RecordArray` that represents the contents of a has-many
relationship.
The `ManyArray` is instantiated lazily the first time the relationship is
requested.
### Inverses
Often, the relationships in Ember Data applications will have
an inverse. For example, imagine the following models are
defined:
```javascript
App.Post = DS.Model.extend({
comments: DS.hasMany('comment')
});
App.Comment = DS.Model.extend({
post: DS.belongsTo('post')
});
```
If you created a new instance of `App.Post` and added
a `App.Comment` record to its `comments` has-many
relationship, you would expect the comment's `post`
property to be set to the post that contained
the has-many.
We call the record to which a relationship belongs the
relationship's _owner_.
@class ManyArray
@namespace DS
@extends DS.RecordArray
*/
__exports__["default"] = RecordArray.extend({
init: function() {
this._super.apply(this, arguments);
},
/**
`true` if the relationship is polymorphic, `false` otherwise.
@property {Boolean} isPolymorphic
@private
*/
isPolymorphic: false,
// LOADING STATE
isLoaded: false,
/**
The relationship which manages this array.
@property {DS.Model} owner
@private
*/
relationship: null,
/**
Used for async `hasMany` arrays
to keep track of when they will resolve.
@property {Ember.RSVP.Promise} promise
@private
*/
promise: null,
/**
@method loadingRecordsCount
@param {Number} count
@private
*/
loadingRecordsCount: function(count) {
this.loadingRecordsCount = count;
},
/**
@method loadedRecord
@private
*/
loadedRecord: function() {
this.loadingRecordsCount--;
if (this.loadingRecordsCount === 0) {
set(this, 'isLoaded', true);
this.trigger('didLoad');
}
},
replaceContent: function(idx, amt, objects){
var records;
if (amt > 0){
records = get(this, 'content').slice(idx, idx+amt);
this.get('relationship').removeRecords(records);
}
if (objects){
this.get('relationship').addRecords(objects, idx);
}
},
/**
@method reload
@public
*/
reload: function() {
return this.relationship.reload();
},
/**
Create a child record within the owner
@method createRecord
@private
@param {Object} hash
@return {DS.Model} record
*/
createRecord: function(hash) {
var store = get(this, 'store');
var type = get(this, 'type');
var record;
Ember.assert("You cannot add '" + type.typeKey + "' records to this polymorphic relationship.", !get(this, 'isPolymorphic'));
record = store.createRecord.call(store, type, hash);
this.pushObject(record);
return record;
}
});
});
define("ember-data/system/record_arrays/record_array",
["ember-data/system/promise_proxies","exports"],
function(__dependency1__, __exports__) {
"use strict";
/**
@module ember-data
*/
var PromiseArray = __dependency1__.PromiseArray;
var get = Ember.get;
/**
A record array is an array that contains records of a certain type. The record
array materializes records as needed when they are retrieved for the first
time. You should not create record arrays yourself. Instead, an instance of
`DS.RecordArray` or its subclasses will be returned by your application's store
in response to queries.
@class RecordArray
@namespace DS
@extends Ember.ArrayProxy
@uses Ember.Evented
*/
__exports__["default"] = Ember.ArrayProxy.extend(Ember.Evented, {
/**
The model type contained by this record array.
@property type
@type DS.Model
*/
type: null,
/**
The array of client ids backing the record array. When a
record is requested from the record array, the record
for the client id at the same index is materialized, if
necessary, by the store.
@property content
@private
@type Ember.Array
*/
content: null,
/**
The flag to signal a `RecordArray` is currently loading data.
Example
```javascript
var people = store.all('person');
people.get('isLoaded'); // true
```
@property isLoaded
@type Boolean
*/
isLoaded: false,
/**
The flag to signal a `RecordArray` is currently loading data.
Example
```javascript
var people = store.all('person');
people.get('isUpdating'); // false
people.update();
people.get('isUpdating'); // true
```
@property isUpdating
@type Boolean
*/
isUpdating: false,
/**
The store that created this record array.
@property store
@private
@type DS.Store
*/
store: null,
/**
Retrieves an object from the content by index.
@method objectAtContent
@private
@param {Number} index
@return {DS.Model} record
*/
objectAtContent: function(index) {
var content = get(this, 'content');
return content.objectAt(index);
},
/**
Used to get the latest version of all of the records in this array
from the adapter.
Example
```javascript
var people = store.all('person');
people.get('isUpdating'); // false
people.update();
people.get('isUpdating'); // true
```
@method update
*/
update: function() {
if (get(this, 'isUpdating')) { return; }
var store = get(this, 'store');
var type = get(this, 'type');
return store.fetchAll(type, this);
},
/**
Adds a record to the `RecordArray` without duplicates
@method addRecord
@private
@param {DS.Model} record
@param {DS.Model} an optional index to insert at
*/
addRecord: function(record, idx) {
var content = get(this, 'content');
if (idx === undefined) {
content.addObject(record);
} else {
if (!content.contains(record)) {
content.insertAt(idx, record);
}
}
},
/**
Adds a record to the `RecordArray`, but allows duplicates
@method pushRecord
@private
@param {DS.Model} record
*/
pushRecord: function(record) {
get(this, 'content').pushObject(record);
},
/**
Removes a record to the `RecordArray`.
@method removeRecord
@private
@param {DS.Model} record
*/
removeRecord: function(record) {
get(this, 'content').removeObject(record);
},
/**
Saves all of the records in the `RecordArray`.
Example
```javascript
var messages = store.all('message');
messages.forEach(function(message) {
message.set('hasBeenSeen', true);
});
messages.save();
```
@method save
@return {DS.PromiseArray} promise
*/
save: function() {
var promiseLabel = "DS: RecordArray#save " + get(this, 'type');
var promise = Ember.RSVP.all(this.invoke("save"), promiseLabel).then(function(array) {
return Ember.A(array);
}, null, "DS: RecordArray#save apply Ember.NativeArray");
return PromiseArray.create({ promise: promise });
},
_dissociateFromOwnRecords: function() {
var array = this;
this.forEach(function(record){
var recordArrays = record._recordArrays;
if (recordArrays) {
recordArrays["delete"](array);
}
});
},
willDestroy: function(){
this._dissociateFromOwnRecords();
this._super();
}
});
});
define("ember-data/system/relationship-meta",
["ember-inflector/system","exports"],
function(__dependency1__, __exports__) {
"use strict";
var singularize = __dependency1__.singularize;
function typeForRelationshipMeta(store, meta) {
var typeKey, type;
typeKey = meta.type || meta.key;
if (typeof typeKey === 'string') {
if (meta.kind === 'hasMany') {
typeKey = singularize(typeKey);
}
type = store.modelFor(typeKey);
} else {
type = meta.type;
}
return type;
}
__exports__.typeForRelationshipMeta = typeForRelationshipMeta;function relationshipFromMeta(store, meta) {
return {
key: meta.key,
kind: meta.kind,
type: typeForRelationshipMeta(store, meta),
options: meta.options,
parentType: meta.parentType,
isRelationship: true
};
}
__exports__.relationshipFromMeta = relationshipFromMeta;
});
define("ember-data/system/relationships",
["./relationships/belongs_to","./relationships/has_many","ember-data/system/relationships/ext","exports"],
function(__dependency1__, __dependency2__, __dependency3__, __exports__) {
"use strict";
/**
@module ember-data
*/
var belongsTo = __dependency1__["default"];
var hasMany = __dependency2__["default"];
__exports__.belongsTo = belongsTo;
__exports__.hasMany = hasMany;
});
define("ember-data/system/relationships/belongs_to",
["ember-data/system/model","exports"],
function(__dependency1__, __exports__) {
"use strict";
var Model = __dependency1__.Model;
/**
`DS.belongsTo` is used to define One-To-One and One-To-Many
relationships on a [DS.Model](/api/data/classes/DS.Model.html).
`DS.belongsTo` takes an optional hash as a second parameter, currently
supported options are:
- `async`: A boolean value used to explicitly declare this to be an async relationship.
- `inverse`: A string used to identify the inverse property on a
related model in a One-To-Many relationship. See [Explicit Inverses](#toc_explicit-inverses)
#### One-To-One
To declare a one-to-one relationship between two models, use
`DS.belongsTo`:
```javascript
App.User = DS.Model.extend({
profile: DS.belongsTo('profile')
});
App.Profile = DS.Model.extend({
user: DS.belongsTo('user')
});
```
#### One-To-Many
To declare a one-to-many relationship between two models, use
`DS.belongsTo` in combination with `DS.hasMany`, like this:
```javascript
App.Post = DS.Model.extend({
comments: DS.hasMany('comment')
});
App.Comment = DS.Model.extend({
post: DS.belongsTo('post')
});
```
@namespace
@method belongsTo
@for DS
@param {String or DS.Model} type the model type of the relationship
@param {Object} options a hash of options
@return {Ember.computed} relationship
*/
function belongsTo(type, options) {
if (typeof type === 'object') {
options = type;
type = undefined;
} else {
Ember.assert("The first argument to DS.belongsTo must be a string representing a model type key, e.g. use DS.belongsTo('person') to define a relation to the App.Person model", !!type && (typeof type === 'string' || Model.detect(type)));
}
options = options || {};
var meta = {
type: type,
isRelationship: true,
options: options,
kind: 'belongsTo',
key: null
};
return Ember.computed(function(key, value) {
if (arguments.length>1) {
if ( value === undefined ) {
value = null;
}
if (value && value.then) {
this._relationships[key].setRecordPromise(value);
} else {
this._relationships[key].setRecord(value);
}
}
return this._relationships[key].getRecord();
}).meta(meta);
}
/**
These observers observe all `belongsTo` relationships on the record. See
`relationships/ext` to see how these observers get their dependencies.
@class Model
@namespace DS
*/
Model.reopen({
notifyBelongsToAdded: function(key, relationship) {
this.notifyPropertyChange(key);
},
notifyBelongsToRemoved: function(key) {
this.notifyPropertyChange(key);
}
});
__exports__["default"] = belongsTo;
});
define("ember-data/system/relationships/ext",
["ember-data/system/relationship-meta","ember-data/system/model","ember-data/system/map"],
function(__dependency1__, __dependency2__, __dependency3__) {
"use strict";
var typeForRelationshipMeta = __dependency1__.typeForRelationshipMeta;
var relationshipFromMeta = __dependency1__.relationshipFromMeta;
var Model = __dependency2__.Model;
var Map = __dependency3__.Map;
var MapWithDefault = __dependency3__.MapWithDefault;
var get = Ember.get;
var filter = Ember.ArrayPolyfills.filter;
/**
@module ember-data
*/
/*
This file defines several extensions to the base `DS.Model` class that
add support for one-to-many relationships.
*/
/**
@class Model
@namespace DS
*/
Model.reopen({
/**
This Ember.js hook allows an object to be notified when a property
is defined.
In this case, we use it to be notified when an Ember Data user defines a
belongs-to relationship. In that case, we need to set up observers for
each one, allowing us to track relationship changes and automatically
reflect changes in the inverse has-many array.
This hook passes the class being set up, as well as the key and value
being defined. So, for example, when the user does this:
```javascript
DS.Model.extend({
parent: DS.belongsTo('user')
});
```
This hook would be called with "parent" as the key and the computed
property returned by `DS.belongsTo` as the value.
@method didDefineProperty
@param {Object} proto
@param {String} key
@param {Ember.ComputedProperty} value
*/
didDefineProperty: function(proto, key, value) {
// Check if the value being set is a computed property.
if (value instanceof Ember.ComputedProperty) {
// If it is, get the metadata for the relationship. This is
// populated by the `DS.belongsTo` helper when it is creating
// the computed property.
var meta = value.meta();
meta.parentType = proto.constructor;
}
}
});
/*
These DS.Model extensions add class methods that provide relationship
introspection abilities about relationships.
A note about the computed properties contained here:
**These properties are effectively sealed once called for the first time.**
To avoid repeatedly doing expensive iteration over a model's fields, these
values are computed once and then cached for the remainder of the runtime of
your application.
If your application needs to modify a class after its initial definition
(for example, using `reopen()` to add additional attributes), make sure you
do it before using your model with the store, which uses these properties
extensively.
*/
Model.reopenClass({
/**
For a given relationship name, returns the model type of the relationship.
For example, if you define a model like this:
```javascript
App.Post = DS.Model.extend({
comments: DS.hasMany('comment')
});
```
Calling `App.Post.typeForRelationship('comments')` will return `App.Comment`.
@method typeForRelationship
@static
@param {String} name the name of the relationship
@return {subclass of DS.Model} the type of the relationship, or undefined
*/
typeForRelationship: function(name) {
var relationship = get(this, 'relationshipsByName').get(name);
return relationship && relationship.type;
},
inverseMap: Ember.computed(function() {
return Object.create(null);
}),
/*
Find the relationship which is the inverse of the one asked for.
For example, if you define models like this:
```javascript
App.Post = DS.Model.extend({
comments: DS.hasMany('message')
});
App.Message = DS.Model.extend({
owner: DS.belongsTo('post')
});
```
App.Post.inverseFor('comments') -> {type: App.Message, name:'owner', kind:'belongsTo'}
App.Message.inverseFor('owner') -> {type: App.Post, name:'comments', kind:'hasMany'}
@method inverseFor
@static
@param {String} name the name of the relationship
@return {Object} the inverse relationship, or null
*/
inverseFor: function(name) {
var inverseMap = get(this, 'inverseMap');
if (inverseMap[name]) {
return inverseMap[name];
} else {
var inverse = this._findInverseFor(name);
inverseMap[name] = inverse;
return inverse;
}
},
//Calculate the inverse, ignoring the cache
_findInverseFor: function(name) {
var inverseType = this.typeForRelationship(name);
if (!inverseType) {
return null;
}
//If inverse is manually specified to be null, like `comments: DS.hasMany('message', {inverse: null})`
var options = this.metaForProperty(name).options;
if (options.inverse === null) { return null; }
var inverseName, inverseKind, inverse;
//If inverse is specified manually, return the inverse
if (options.inverse) {
inverseName = options.inverse;
inverse = Ember.get(inverseType, 'relationshipsByName').get(inverseName);
Ember.assert("We found no inverse relationships by the name of '" + inverseName + "' on the '" + inverseType.typeKey +
"' model. This is most likely due to a missing attribute on your model definition.", !Ember.isNone(inverse));
inverseKind = inverse.kind;
} else {
//No inverse was specified manually, we need to use a heuristic to guess one
var possibleRelationships = findPossibleInverses(this, inverseType);
if (possibleRelationships.length === 0) { return null; }
var filteredRelationships = filter.call(possibleRelationships, function(possibleRelationship) {
var optionsForRelationship = inverseType.metaForProperty(possibleRelationship.name).options;
return name === optionsForRelationship.inverse;
});
Ember.assert("You defined the '" + name + "' relationship on " + this + ", but you defined the inverse relationships of type " +
inverseType.toString() + " multiple times. Look at http://emberjs.com/guides/models/defining-models/#toc_explicit-inverses for how to explicitly specify inverses",
filteredRelationships.length < 2);
if (filteredRelationships.length === 1 ) {
possibleRelationships = filteredRelationships;
}
Ember.assert("You defined the '" + name + "' relationship on " + this + ", but multiple possible inverse relationships of type " +
this + " were found on " + inverseType + ". Look at http://emberjs.com/guides/models/defining-models/#toc_explicit-inverses for how to explicitly specify inverses",
possibleRelationships.length === 1);
inverseName = possibleRelationships[0].name;
inverseKind = possibleRelationships[0].kind;
}
function findPossibleInverses(type, inverseType, relationshipsSoFar) {
var possibleRelationships = relationshipsSoFar || [];
var relationshipMap = get(inverseType, 'relationships');
if (!relationshipMap) { return; }
var relationships = relationshipMap.get(type);
relationships = filter.call(relationships, function(relationship) {
var optionsForRelationship = inverseType.metaForProperty(relationship.name).options;
if (!optionsForRelationship.inverse){
return true;
}
return name === optionsForRelationship.inverse;
});
if (relationships) {
possibleRelationships.push.apply(possibleRelationships, relationships);
}
//Recurse to support polymorphism
if (type.superclass) {
findPossibleInverses(type.superclass, inverseType, possibleRelationships);
}
return possibleRelationships;
}
return {
type: inverseType,
name: inverseName,
kind: inverseKind
};
},
/**
The model's relationships as a map, keyed on the type of the
relationship. The value of each entry is an array containing a descriptor
for each relationship with that type, describing the name of the relationship
as well as the type.
For example, given the following model definition:
```javascript
App.Blog = DS.Model.extend({
users: DS.hasMany('user'),
owner: DS.belongsTo('user'),
posts: DS.hasMany('post')
});
```
This computed property would return a map describing these
relationships, like this:
```javascript
var relationships = Ember.get(App.Blog, 'relationships');
relationships.get(App.User);
//=> [ { name: 'users', kind: 'hasMany' },
// { name: 'owner', kind: 'belongsTo' } ]
relationships.get(App.Post);
//=> [ { name: 'posts', kind: 'hasMany' } ]
```
@property relationships
@static
@type Ember.Map
@readOnly
*/
relationships: Ember.computed(function() {
var map = new MapWithDefault({
defaultValue: function() { return []; }
});
// Loop through each computed property on the class
this.eachComputedProperty(function(name, meta) {
// If the computed property is a relationship, add
// it to the map.
if (meta.isRelationship) {
meta.key = name;
var relationshipsForType = map.get(typeForRelationshipMeta(this.store, meta));
relationshipsForType.push({
name: name,
kind: meta.kind
});
}
});
return map;
}).volatile().readOnly(),
/**
A hash containing lists of the model's relationships, grouped
by the relationship kind. For example, given a model with this
definition:
```javascript
App.Blog = DS.Model.extend({
users: DS.hasMany('user'),
owner: DS.belongsTo('user'),
posts: DS.hasMany('post')
});
```
This property would contain the following:
```javascript
var relationshipNames = Ember.get(App.Blog, 'relationshipNames');
relationshipNames.hasMany;
//=> ['users', 'posts']
relationshipNames.belongsTo;
//=> ['owner']
```
@property relationshipNames
@static
@type Object
@readOnly
*/
relationshipNames: Ember.computed(function() {
var names = {
hasMany: [],
belongsTo: []
};
this.eachComputedProperty(function(name, meta) {
if (meta.isRelationship) {
names[meta.kind].push(name);
}
});
return names;
}),
/**
An array of types directly related to a model. Each type will be
included once, regardless of the number of relationships it has with
the model.
For example, given a model with this definition:
```javascript
App.Blog = DS.Model.extend({
users: DS.hasMany('user'),
owner: DS.belongsTo('user'),
posts: DS.hasMany('post')
});
```
This property would contain the following:
```javascript
var relatedTypes = Ember.get(App.Blog, 'relatedTypes');
//=> [ App.User, App.Post ]
```
@property relatedTypes
@static
@type Ember.Array
@readOnly
*/
relatedTypes: Ember.computed(function() {
var type;
var types = Ember.A();
// Loop through each computed property on the class,
// and create an array of the unique types involved
// in relationships
this.eachComputedProperty(function(name, meta) {
if (meta.isRelationship) {
meta.key = name;
type = typeForRelationshipMeta(this.store, meta);
Ember.assert("You specified a hasMany (" + meta.type + ") on " + meta.parentType + " but " + meta.type + " was not found.", type);
if (!types.contains(type)) {
Ember.assert("Trying to sideload " + name + " on " + this.toString() + " but the type doesn't exist.", !!type);
types.push(type);
}
}
});
return types;
}).volatile().readOnly(),
/**
A map whose keys are the relationships of a model and whose values are
relationship descriptors.
For example, given a model with this
definition:
```javascript
App.Blog = DS.Model.extend({
users: DS.hasMany('user'),
owner: DS.belongsTo('user'),
posts: DS.hasMany('post')
});
```
This property would contain the following:
```javascript
var relationshipsByName = Ember.get(App.Blog, 'relationshipsByName');
relationshipsByName.get('users');
//=> { key: 'users', kind: 'hasMany', type: App.User }
relationshipsByName.get('owner');
//=> { key: 'owner', kind: 'belongsTo', type: App.User }
```
@property relationshipsByName
@static
@type Ember.Map
@readOnly
*/
relationshipsByName: Ember.computed(function() {
var map = Map.create();
this.eachComputedProperty(function(name, meta) {
if (meta.isRelationship) {
meta.key = name;
var relationship = relationshipFromMeta(this.store, meta);
relationship.type = typeForRelationshipMeta(this.store, meta);
map.set(name, relationship);
}
});
return map;
}).volatile().readOnly(),
/**
A map whose keys are the fields of the model and whose values are strings
describing the kind of the field. A model's fields are the union of all of its
attributes and relationships.
For example:
```javascript
App.Blog = DS.Model.extend({
users: DS.hasMany('user'),
owner: DS.belongsTo('user'),
posts: DS.hasMany('post'),
title: DS.attr('string')
});
var fields = Ember.get(App.Blog, 'fields');
fields.forEach(function(field, kind) {
console.log(field, kind);
});
// prints:
// users, hasMany
// owner, belongsTo
// posts, hasMany
// title, attribute
```
@property fields
@static
@type Ember.Map
@readOnly
*/
fields: Ember.computed(function() {
var map = Map.create();
this.eachComputedProperty(function(name, meta) {
if (meta.isRelationship) {
map.set(name, meta.kind);
} else if (meta.isAttribute) {
map.set(name, 'attribute');
}
});
return map;
}).readOnly(),
/**
Given a callback, iterates over each of the relationships in the model,
invoking the callback with the name of each relationship and its relationship
descriptor.
@method eachRelationship
@static
@param {Function} callback the callback to invoke
@param {any} binding the value to which the callback's `this` should be bound
*/
eachRelationship: function(callback, binding) {
get(this, 'relationshipsByName').forEach(function(relationship, name) {
callback.call(binding, name, relationship);
});
},
/**
Given a callback, iterates over each of the types related to a model,
invoking the callback with the related type's class. Each type will be
returned just once, regardless of how many different relationships it has
with a model.
@method eachRelatedType
@static
@param {Function} callback the callback to invoke
@param {any} binding the value to which the callback's `this` should be bound
*/
eachRelatedType: function(callback, binding) {
get(this, 'relatedTypes').forEach(function(type) {
callback.call(binding, type);
});
},
determineRelationshipType: function(knownSide) {
var knownKey = knownSide.key;
var knownKind = knownSide.kind;
var inverse = this.inverseFor(knownKey);
var key, otherKind;
if (!inverse) {
return knownKind === 'belongsTo' ? 'oneToNone' : 'manyToNone';
}
key = inverse.name;
otherKind = inverse.kind;
if (otherKind === 'belongsTo') {
return knownKind === 'belongsTo' ? 'oneToOne' : 'manyToOne';
} else {
return knownKind === 'belongsTo' ? 'oneToMany' : 'manyToMany';
}
}
});
Model.reopen({
/**
Given a callback, iterates over each of the relationships in the model,
invoking the callback with the name of each relationship and its relationship
descriptor.
@method eachRelationship
@param {Function} callback the callback to invoke
@param {any} binding the value to which the callback's `this` should be bound
*/
eachRelationship: function(callback, binding) {
this.constructor.eachRelationship(callback, binding);
},
relationshipFor: function(name) {
return get(this.constructor, 'relationshipsByName').get(name);
},
inverseFor: function(key) {
return this.constructor.inverseFor(key);
}
});
});
define("ember-data/system/relationships/has_many",
["ember-data/system/model","exports"],
function(__dependency1__, __exports__) {
"use strict";
/**
@module ember-data
*/
var Model = __dependency1__.Model;
/**
`DS.hasMany` is used to define One-To-Many and Many-To-Many
relationships on a [DS.Model](/api/data/classes/DS.Model.html).
`DS.hasMany` takes an optional hash as a second parameter, currently
supported options are:
- `async`: A boolean value used to explicitly declare this to be an async relationship.
- `inverse`: A string used to identify the inverse property on a related model.
#### One-To-Many
To declare a one-to-many relationship between two models, use
`DS.belongsTo` in combination with `DS.hasMany`, like this:
```javascript
App.Post = DS.Model.extend({
comments: DS.hasMany('comment')
});
App.Comment = DS.Model.extend({
post: DS.belongsTo('post')
});
```
#### Many-To-Many
To declare a many-to-many relationship between two models, use
`DS.hasMany`:
```javascript
App.Post = DS.Model.extend({
tags: DS.hasMany('tag')
});
App.Tag = DS.Model.extend({
posts: DS.hasMany('post')
});
```
#### Explicit Inverses
Ember Data will do its best to discover which relationships map to
one another. In the one-to-many code above, for example, Ember Data
can figure out that changing the `comments` relationship should update
the `post` relationship on the inverse because post is the only
relationship to that model.
However, sometimes you may have multiple `belongsTo`/`hasManys` for the
same type. You can specify which property on the related model is
the inverse using `DS.hasMany`'s `inverse` option:
```javascript
var belongsTo = DS.belongsTo,
hasMany = DS.hasMany;
App.Comment = DS.Model.extend({
onePost: belongsTo('post'),
twoPost: belongsTo('post'),
redPost: belongsTo('post'),
bluePost: belongsTo('post')
});
App.Post = DS.Model.extend({
comments: hasMany('comment', {
inverse: 'redPost'
})
});
```
You can also specify an inverse on a `belongsTo`, which works how
you'd expect.
@namespace
@method hasMany
@for DS
@param {String or DS.Model} type the model type of the relationship
@param {Object} options a hash of options
@return {Ember.computed} relationship
*/
function hasMany(type, options) {
if (typeof type === 'object') {
options = type;
type = undefined;
}
options = options || {};
// Metadata about relationships is stored on the meta of
// the relationship. This is used for introspection and
// serialization. Note that `key` is populated lazily
// the first time the CP is called.
var meta = {
type: type,
isRelationship: true,
options: options,
kind: 'hasMany',
key: null
};
return Ember.computed(function(key) {
var relationship = this._relationships[key];
return relationship.getRecords();
}).meta(meta).readOnly();
}
Model.reopen({
notifyHasManyAdded: function(key, record, idx) {
var relationship = this._relationships[key];
var manyArray = relationship.manyArray;
manyArray.addRecord(record, idx);
//We need to notifyPropertyChange in the adding case because we need to make sure
//we fetch the newly added record in case it is unloaded
//TODO(Igor): Consider whether we could do this only if the record state is unloaded
this.notifyPropertyChange(key);
},
notifyHasManyRemoved: function(key, record) {
var relationship = this._relationships[key];
var manyArray = relationship.manyArray;
manyArray.removeRecord(record);
}
});
__exports__["default"] = hasMany;
});
define("ember-data/system/relationships/relationship",
["ember-data/system/promise_proxies","ember-data/system/map","exports"],
function(__dependency1__, __dependency2__, __exports__) {
"use strict";
var PromiseManyArray = __dependency1__.PromiseManyArray;
var PromiseObject = __dependency1__.PromiseObject;
var OrderedSet = __dependency2__.OrderedSet;
var Relationship = function(store, record, inverseKey, relationshipMeta) {
this.members = new OrderedSet();
this.store = store;
this.key = relationshipMeta.key;
this.inverseKey = inverseKey;
this.record = record;
this.key = relationshipMeta.key;
this.isAsync = relationshipMeta.options.async;
this.relationshipMeta = relationshipMeta;
//This probably breaks for polymorphic relationship in complex scenarios, due to
//multiple possible typeKeys
this.inverseKeyForImplicit = this.store.modelFor(this.record.constructor).typeKey + this.key;
//Cached promise when fetching the relationship from a link
this.linkPromise = null;
};
Relationship.prototype = {
constructor: Relationship,
destroy: Ember.K,
clear: function() {
this.members.forEach(function(member) {
this.removeRecord(member);
}, this);
},
disconnect: function(){
this.members.forEach(function(member) {
this.removeRecordFromInverse(member);
}, this);
},
reconnect: function(){
this.members.forEach(function(member) {
this.addRecordToInverse(member);
}, this);
},
removeRecords: function(records){
var that = this;
records.forEach(function(record){
that.removeRecord(record);
});
},
addRecords: function(records, idx){
var that = this;
records.forEach(function(record){
that.addRecord(record, idx);
if (idx !== undefined) {
idx++;
}
});
},
addRecord: function(record, idx) {
if (!this.members.has(record)) {
this.members.add(record);
this.notifyRecordRelationshipAdded(record, idx);
if (this.inverseKey) {
record._relationships[this.inverseKey].addRecord(this.record);
} else {
if (!record._implicitRelationships[this.inverseKeyForImplicit]) {
record._implicitRelationships[this.inverseKeyForImplicit] = new Relationship(this.store, record, this.key, {options:{}});
}
record._implicitRelationships[this.inverseKeyForImplicit].addRecord(this.record);
}
this.record.updateRecordArrays();
}
},
removeRecord: function(record) {
if (this.members.has(record)) {
this.removeRecordFromOwn(record);
if (this.inverseKey) {
this.removeRecordFromInverse(record);
} else {
if (record._implicitRelationships[this.inverseKeyForImplicit]) {
record._implicitRelationships[this.inverseKeyForImplicit].removeRecord(this.record);
}
}
}
},
addRecordToInverse: function(record) {
if (this.inverseKey) {
record._relationships[this.inverseKey].addRecord(this.record);
}
},
removeRecordFromInverse: function(record) {
var inverseRelationship = record._relationships[this.inverseKey];
//Need to check for existence, as the record might unloading at the moment
if (inverseRelationship) {
inverseRelationship.removeRecordFromOwn(this.record);
}
},
removeRecordFromOwn: function(record) {
this.members["delete"](record);
this.notifyRecordRelationshipRemoved(record);
this.record.updateRecordArrays();
},
updateLink: function(link) {
if (link !== this.link) {
this.link = link;
this.linkPromise = null;
this.record.notifyPropertyChange(this.key);
}
},
findLink: function() {
if (this.linkPromise) {
return this.linkPromise;
} else {
var promise = this.fetchLink();
this.linkPromise = promise;
return promise.then(function(result) {
return result;
});
}
},
updateRecordsFromAdapter: function(records) {
//TODO Once we have adapter support, we need to handle updated and canonical changes
this.computeChanges(records);
},
notifyRecordRelationshipAdded: Ember.K,
notifyRecordRelationshipRemoved: Ember.K
};
var ManyRelationship = function(store, record, inverseKey, relationshipMeta) {
this._super$constructor(store, record, inverseKey, relationshipMeta);
this.belongsToType = relationshipMeta.type;
this.manyArray = store.recordArrayManager.createManyArray(this.belongsToType, Ember.A());
this.manyArray.relationship = this;
this.isPolymorphic = relationshipMeta.options.polymorphic;
this.manyArray.isPolymorphic = this.isPolymorphic;
};
ManyRelationship.prototype = Object.create(Relationship.prototype);
ManyRelationship.prototype.constructor = ManyRelationship;
ManyRelationship.prototype._super$constructor = Relationship;
ManyRelationship.prototype.destroy = function() {
this.manyArray.destroy();
};
ManyRelationship.prototype.notifyRecordRelationshipAdded = function(record, idx) {
Ember.assert("You cannot add '" + record.constructor.typeKey + "' records to this relationship (only '" + this.belongsToType.typeKey + "' allowed)", !this.belongsToType || record instanceof this.belongsToType);
this.record.notifyHasManyAdded(this.key, record, idx);
};
ManyRelationship.prototype.notifyRecordRelationshipRemoved = function(record) {
this.record.notifyHasManyRemoved(this.key, record);
};
ManyRelationship.prototype.reload = function() {
var self = this;
if (this.link) {
return this.fetchLink();
} else {
return this.store.scheduleFetchMany(this.manyArray.toArray()).then(function() {
//Goes away after the manyArray refactor
self.manyArray.set('isLoaded', true);
return self.manyArray;
});
}
};
ManyRelationship.prototype.computeChanges = function(records) {
var members = this.members;
var recordsToRemove = [];
var length;
var record;
var i;
records = setForArray(records);
members.forEach(function(member) {
if (records.has(member)) return;
recordsToRemove.push(member);
});
this.removeRecords(recordsToRemove);
var hasManyArray = this.manyArray;
// Using records.toArray() since currently using
// removeRecord can modify length, messing stuff up
// forEach since it directly looks at "length" each
// iteration
records = records.toArray();
length = records.length;
for (i = 0; i < length; i++){
record = records[i];
//Need to preserve the order of incoming records
if (hasManyArray.objectAt(i) === record ) {
continue;
}
this.removeRecord(record);
this.addRecord(record, i);
}
};
ManyRelationship.prototype.fetchLink = function() {
var self = this;
return this.store.findHasMany(this.record, this.link, this.relationshipMeta).then(function(records){
self.updateRecordsFromAdapter(records);
return self.manyArray;
});
};
ManyRelationship.prototype.findRecords = function() {
var manyArray = this.manyArray;
return this.store.findMany(manyArray.toArray()).then(function(){
//Goes away after the manyArray refactor
manyArray.set('isLoaded', true);
return manyArray;
});
};
ManyRelationship.prototype.getRecords = function() {
if (this.isAsync) {
var self = this;
var promise;
if (this.link) {
promise = this.findLink().then(function() {
return self.findRecords();
});
} else {
promise = this.findRecords();
}
return PromiseManyArray.create({
content: this.manyArray,
promise: promise
});
} else {
Ember.assert("You looked up the '" + this.key + "' relationship on a '" + this.record.constructor.typeKey + "' with id " + this.record.get('id') + " but some of the associated records were not loaded. Either make sure they are all loaded together with the parent record, or specify that the relationship is async (`DS.hasMany({ async: true })`)", this.manyArray.isEvery('isEmpty', false));
this.manyArray.set('isLoaded', true);
return this.manyArray;
}
};
var BelongsToRelationship = function(store, record, inverseKey, relationshipMeta) {
this._super$constructor(store, record, inverseKey, relationshipMeta);
this.record = record;
this.key = relationshipMeta.key;
this.inverseRecord = null;
};
BelongsToRelationship.prototype = Object.create(Relationship.prototype);
BelongsToRelationship.prototype.constructor = BelongsToRelationship;
BelongsToRelationship.prototype._super$constructor = Relationship;
BelongsToRelationship.prototype.setRecord = function(newRecord) {
if (newRecord) {
this.addRecord(newRecord);
} else if (this.inverseRecord) {
this.removeRecord(this.inverseRecord);
}
};
BelongsToRelationship.prototype._super$addRecord = Relationship.prototype.addRecord;
BelongsToRelationship.prototype.addRecord = function(newRecord) {
if (this.members.has(newRecord)){ return;}
var type = this.relationshipMeta.type;
Ember.assert("You can only add a '" + type.typeKey + "' record to this relationship", newRecord instanceof type);
if (this.inverseRecord) {
this.removeRecord(this.inverseRecord);
}
this.inverseRecord = newRecord;
this._super$addRecord(newRecord);
};
BelongsToRelationship.prototype.setRecordPromise = function(newPromise) {
var content = newPromise.get && newPromise.get('content');
Ember.assert("You passed in a promise that did not originate from an EmberData relationship. You can only pass promises that come from a belongsTo or hasMany relationship to the get call.", content !== undefined);
this.setRecord(content);
};
BelongsToRelationship.prototype.notifyRecordRelationshipAdded = function(newRecord) {
this.record.notifyBelongsToAdded(this.key, this);
};
BelongsToRelationship.prototype.notifyRecordRelationshipRemoved = function(record) {
this.record.notifyBelongsToRemoved(this.key, this);
};
BelongsToRelationship.prototype._super$removeRecordFromOwn = Relationship.prototype.removeRecordFromOwn;
BelongsToRelationship.prototype.removeRecordFromOwn = function(record) {
if (!this.members.has(record)){ return;}
this._super$removeRecordFromOwn(record);
this.inverseRecord = null;
};
BelongsToRelationship.prototype.findRecord = function() {
if (this.inverseRecord) {
return this.store._findByRecord(this.inverseRecord);
} else {
return Ember.RSVP.Promise.resolve(null);
}
};
BelongsToRelationship.prototype.fetchLink = function() {
var self = this;
return this.store.findBelongsTo(this.record, this.link, this.relationshipMeta).then(function(record){
self.addRecord(record);
return record;
});
};
BelongsToRelationship.prototype.getRecord = function() {
if (this.isAsync) {
var promise;
if (this.link){
var self = this;
promise = this.findLink().then(function() {
return self.findRecord();
});
} else {
promise = this.findRecord();
}
return PromiseObject.create({
promise: promise,
content: this.inverseRecord
});
} else {
Ember.assert("You looked up the '" + this.key + "' relationship on a '" + this.record.constructor.typeKey + "' with id " + this.record.get('id') + " but some of the associated records were not loaded. Either make sure they are all loaded together with the parent record, or specify that the relationship is async (`DS.belongsTo({ async: true })`)", this.inverseRecord === null || !this.inverseRecord.get('isEmpty'));
return this.inverseRecord;
}
};
function setForArray(array) {
var set = new OrderedSet();
if (array) {
for (var i=0, l=array.length; i<l; i++) {
set.add(array[i]);
}
}
return set;
}
var createRelationshipFor = function(record, relationshipMeta, store){
var inverseKey;
var inverse = record.constructor.inverseFor(relationshipMeta.key);
if (inverse) {
inverseKey = inverse.name;
}
if (relationshipMeta.kind === 'hasMany'){
return new ManyRelationship(store, record, inverseKey, relationshipMeta);
}
else {
return new BelongsToRelationship(store, record, inverseKey, relationshipMeta);
}
};
__exports__.Relationship = Relationship;
__exports__.ManyRelationship = ManyRelationship;
__exports__.BelongsToRelationship = BelongsToRelationship;
__exports__.createRelationshipFor = createRelationshipFor;
});
define("ember-data/system/store",
["ember-data/system/adapter","ember-inflector/system/string","ember-data/system/map","ember-data/system/promise_proxies","exports"],
function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __exports__) {
"use strict";
/*globals Ember*/
/*jshint eqnull:true*/
/**
@module ember-data
*/
var InvalidError = __dependency1__.InvalidError;
var Adapter = __dependency1__.Adapter;
var singularize = __dependency2__.singularize;
var Map = __dependency3__.Map;
var promiseArray = __dependency4__.promiseArray;
var promiseObject = __dependency4__.promiseObject;
var get = Ember.get;
var set = Ember.set;
var once = Ember.run.once;
var isNone = Ember.isNone;
var forEach = Ember.EnumerableUtils.forEach;
var indexOf = Ember.EnumerableUtils.indexOf;
var map = Ember.EnumerableUtils.map;
var Promise = Ember.RSVP.Promise;
var copy = Ember.copy;
var Store, RecordArrayManager, Model;
var camelize = Ember.String.camelize;
// Implementors Note:
//
// The variables in this file are consistently named according to the following
// scheme:
//
// * +id+ means an identifier managed by an external source, provided inside
// the data provided by that source. These are always coerced to be strings
// before being used internally.
// * +clientId+ means a transient numerical identifier generated at runtime by
// the data store. It is important primarily because newly created objects may
// not yet have an externally generated id.
// * +reference+ means a record reference object, which holds metadata about a
// record, even if it has not yet been fully materialized.
// * +type+ means a subclass of DS.Model.
// Used by the store to normalize IDs entering the store. Despite the fact
// that developers may provide IDs as numbers (e.g., `store.find(Person, 1)`),
// it is important that internally we use strings, since IDs may be serialized
// and lose type information. For example, Ember's router may put a record's
// ID into the URL, and if we later try to deserialize that URL and find the
// corresponding record, we will not know if it is a string or a number.
function coerceId(id) {
return id == null ? null : id+'';
}
/**
The store contains all of the data for records loaded from the server.
It is also responsible for creating instances of `DS.Model` that wrap
the individual data for a record, so that they can be bound to in your
Handlebars templates.
Define your application's store like this:
```javascript
MyApp.Store = DS.Store.extend();
```
Most Ember.js applications will only have a single `DS.Store` that is
automatically created by their `Ember.Application`.
You can retrieve models from the store in several ways. To retrieve a record
for a specific id, use `DS.Store`'s `find()` method:
```javascript
store.find('person', 123).then(function (person) {
});
```
If your application has multiple `DS.Store` instances (an unusual case), you can
specify which store should be used:
```javascript
store.find('person', 123).then(function (person) {
});
```
By default, the store will talk to your backend using a standard
REST mechanism. You can customize how the store talks to your
backend by specifying a custom adapter:
```javascript
MyApp.ApplicationAdapter = MyApp.CustomAdapter
```
You can learn more about writing a custom adapter by reading the `DS.Adapter`
documentation.
### Store createRecord() vs. push() vs. pushPayload() vs. update()
The store provides multiple ways to create new record objects. They have
some subtle differences in their use which are detailed below:
[createRecord](#method_createRecord) is used for creating new
records on the client side. This will return a new record in the
`created.uncommitted` state. In order to persist this record to the
backend you will need to call `record.save()`.
[push](#method_push) is used to notify Ember Data's store of new or
updated records that exist in the backend. This will return a record
in the `loaded.saved` state. The primary use-case for `store#push` is
to notify Ember Data about record updates that happen
outside of the normal adapter methods (for example
[SSE](http://dev.w3.org/html5/eventsource/) or [Web
Sockets](http://www.w3.org/TR/2009/WD-websockets-20091222/)).
[pushPayload](#method_pushPayload) is a convenience wrapper for
`store#push` that will deserialize payloads if the
Serializer implements a `pushPayload` method.
[update](#method_update) works like `push`, except it can handle
partial attributes without overwriting the existing record
properties.
Note: When creating a new record using any of the above methods
Ember Data will update `DS.RecordArray`s such as those returned by
`store#all()`, `store#findAll()` or `store#filter()`. This means any
data bindings or computed properties that depend on the RecordArray
will automatically be synced to include the new or updated record
values.
@class Store
@namespace DS
@extends Ember.Object
*/
Store = Ember.Object.extend({
/**
@method init
@private
*/
init: function() {
// internal bookkeeping; not observable
if (!RecordArrayManager) { RecordArrayManager = requireModule("ember-data/system/record_array_manager")["default"]; }
this.typeMaps = {};
this.recordArrayManager = RecordArrayManager.create({
store: this
});
this._pendingSave = [];
//Used to keep track of all the find requests that need to be coalesced
this._pendingFetch = Map.create();
},
/**
The adapter to use to communicate to a backend server or other persistence layer.
This can be specified as an instance, class, or string.
If you want to specify `App.CustomAdapter` as a string, do:
```js
adapter: 'custom'
```
@property adapter
@default DS.RESTAdapter
@type {DS.Adapter|String}
*/
adapter: '-rest',
/**
Returns a JSON representation of the record using a custom
type-specific serializer, if one exists.
The available options are:
* `includeId`: `true` if the record's ID should be included in
the JSON representation
@method serialize
@private
@param {DS.Model} record the record to serialize
@param {Object} options an options hash
*/
serialize: function(record, options) {
return this.serializerFor(record.constructor.typeKey).serialize(record, options);
},
/**
This property returns the adapter, after resolving a possible
string key.
If the supplied `adapter` was a class, or a String property
path resolved to a class, this property will instantiate the
class.
This property is cacheable, so the same instance of a specified
adapter class should be used for the lifetime of the store.
@property defaultAdapter
@private
@return DS.Adapter
*/
defaultAdapter: Ember.computed('adapter', function() {
var adapter = get(this, 'adapter');
Ember.assert('You tried to set `adapter` property to an instance of `DS.Adapter`, where it should be a name or a factory', !(adapter instanceof Adapter));
if (typeof adapter === 'string') {
adapter = this.container.lookup('adapter:' + adapter) || this.container.lookup('adapter:application') || this.container.lookup('adapter:-rest');
}
if (DS.Adapter.detect(adapter)) {
adapter = adapter.create({
container: this.container
});
}
return adapter;
}),
// .....................
// . CREATE NEW RECORD .
// .....................
/**
Create a new record in the current store. The properties passed
to this method are set on the newly created record.
To create a new instance of `App.Post`:
```js
store.createRecord('post', {
title: "Rails is omakase"
});
```
@method createRecord
@param {String} type
@param {Object} properties a hash of properties to set on the
newly created record.
@return {DS.Model} record
*/
createRecord: function(typeName, inputProperties) {
var type = this.modelFor(typeName);
var properties = copy(inputProperties) || {};
// If the passed properties do not include a primary key,
// give the adapter an opportunity to generate one. Typically,
// client-side ID generators will use something like uuid.js
// to avoid conflicts.
if (isNone(properties.id)) {
properties.id = this._generateId(type);
}
// Coerce ID to a string
properties.id = coerceId(properties.id);
var record = this.buildRecord(type, properties.id);
// Move the record out of its initial `empty` state into
// the `loaded` state.
record.loadedData();
// Set the properties specified on the record.
record.setProperties(properties);
return record;
},
/**
If possible, this method asks the adapter to generate an ID for
a newly created record.
@method _generateId
@private
@param {String} type
@return {String} if the adapter can generate one, an ID
*/
_generateId: function(type) {
var adapter = this.adapterFor(type);
if (adapter && adapter.generateIdForRecord) {
return adapter.generateIdForRecord(this);
}
return null;
},
// .................
// . DELETE RECORD .
// .................
/**
For symmetry, a record can be deleted via the store.
Example
```javascript
var post = store.createRecord('post', {
title: "Rails is omakase"
});
store.deleteRecord(post);
```
@method deleteRecord
@param {DS.Model} record
*/
deleteRecord: function(record) {
record.deleteRecord();
},
/**
For symmetry, a record can be unloaded via the store. Only
non-dirty records can be unloaded.
Example
```javascript
store.find('post', 1).then(function(post) {
store.unloadRecord(post);
});
```
@method unloadRecord
@param {DS.Model} record
*/
unloadRecord: function(record) {
record.unloadRecord();
},
// ................
// . FIND RECORDS .
// ................
/**
This is the main entry point into finding records. The first parameter to
this method is the model's name as a string.
---
To find a record by ID, pass the `id` as the second parameter:
```javascript
store.find('person', 1);
```
The `find` method will always return a **promise** that will be resolved
with the record. If the record was already in the store, the promise will
be resolved immediately. Otherwise, the store will ask the adapter's `find`
method to find the necessary data.
The `find` method will always resolve its promise with the same object for
a given type and `id`.
---
You can optionally `preload` specific attributes and relationships that you know of
by passing them as the third argument to find.
For example, if your Ember route looks like `/posts/1/comments/2` and your API route
for the comment also looks like `/posts/1/comments/2` if you want to fetch the comment
without fetching the post you can pass in the post to the `find` call:
```javascript
store.find('comment', 2, {post: 1});
```
If you have access to the post model you can also pass the model itself:
```javascript
store.find('post', 1).then(function (myPostModel) {
store.find('comment', 2, {post: myPostModel});
});
```
This way, your adapter's `find` or `buildURL` method will be able to look up the
relationship on the record and construct the nested URL without having to first
fetch the post.
---
To find all records for a type, call `find` with no additional parameters:
```javascript
store.find('person');
```
This will ask the adapter's `findAll` method to find the records for the
given type, and return a promise that will be resolved once the server
returns the values.
---
To find a record by a query, call `find` with a hash as the second
parameter:
```javascript
store.find('person', { page: 1 });
```
This will ask the adapter's `findQuery` method to find the records for
the query, and return a promise that will be resolved once the server
responds.
@method find
@param {String or subclass of DS.Model} type
@param {Object|String|Integer|null} id
@param {Object} preload - optional set of attributes and relationships passed in either as IDs or as actual models
@return {Promise} promise
*/
find: function(type, id, preload) {
Ember.assert("You need to pass a type to the store's find method", arguments.length >= 1);
Ember.assert("You may not pass `" + id + "` as id to the store's find method", arguments.length === 1 || !Ember.isNone(id));
if (arguments.length === 1) {