summaryrefslogtreecommitdiff
path: root/public/js/libs/ember-data.js
diff options
context:
space:
mode:
Diffstat (limited to 'public/js/libs/ember-data.js')
-rw-r--r--public/js/libs/ember-data.js8894
1 files changed, 8894 insertions, 0 deletions
diff --git a/public/js/libs/ember-data.js b/public/js/libs/ember-data.js
new file mode 100644
index 0000000..798478b
--- /dev/null
+++ b/public/js/libs/ember-data.js
@@ -0,0 +1,8894 @@
+// Version: v0.13-33-g8cf224d
+// Last commit: 8cf224d (2013-06-19 00:23:32 -0400)
+
+
+(function() {
+var define, requireModule;
+
+(function() {
+ var registry = {}, seen = {};
+
+ define = function(name, deps, callback) {
+ registry[name] = { deps: deps, callback: callback };
+ };
+
+ requireModule = function(name) {
+ if (seen[name]) { return seen[name]; }
+ seen[name] = {};
+
+ var mod, deps, callback, reified , exports;
+
+ mod = registry[name];
+
+ if (!mod) {
+ throw new Error("Module '" + name + "' not found.");
+ }
+
+ deps = mod.deps;
+ callback = mod.callback;
+ reified = [];
+ exports;
+
+ for (var i=0, l=deps.length; i<l; i++) {
+ if (deps[i] === 'exports') {
+ reified.push(exports = {});
+ } else {
+ reified.push(requireModule(deps[i]));
+ }
+ }
+
+ var value = callback.apply(this, reified);
+ return seen[name] = exports || value;
+ };
+})();
+(function() {
+/**
+ @module data
+ @main data
+*/
+
+/**
+ All Ember Data methods and functions are defined inside of this namespace.
+
+ @class DS
+ @static
+*/
+
+window.DS = Ember.Namespace.create();
+
+})();
+
+
+
+(function() {
+var set = Ember.set;
+
+/**
+ This code registers an injection for Ember.Application.
+
+ If an Ember.js developer defines a subclass of DS.Store on their application,
+ this code will automatically instantiate it and make it available on the
+ router.
+
+ Additionally, after an application's controllers have been injected, they will
+ each have the store made available to them.
+
+ For example, imagine an Ember.js application with the following classes:
+
+ App.Store = DS.Store.extend({
+ adapter: 'App.MyCustomAdapter'
+ });
+
+ App.PostsController = Ember.ArrayController.extend({
+ // ...
+ });
+
+ When the application is initialized, `App.Store` will automatically be
+ instantiated, and the instance of `App.PostsController` will have its `store`
+ property set to that instance.
+
+ Note that this code will only be run if the `ember-application` package is
+ loaded. If Ember Data is being used in an environment other than a
+ typical application (e.g., node.js where only `ember-runtime` is available),
+ this code will be ignored.
+*/
+
+Ember.onLoad('Ember.Application', function(Application) {
+ Application.initializer({
+ name: "store",
+
+ initialize: function(container, application) {
+ application.register('store:main', application.Store);
+
+ // Eagerly generate the store so defaultStore is populated.
+ // TODO: Do this in a finisher hook
+ container.lookup('store:main');
+ }
+ });
+
+ Application.initializer({
+ name: "injectStore",
+
+ initialize: function(container, application) {
+ application.inject('controller', 'store', 'store:main');
+ application.inject('route', 'store', 'store:main');
+ }
+ });
+});
+
+})();
+
+
+
+(function() {
+/**
+ * Date.parse with progressive enhancement for ISO 8601 <https://github.com/csnover/js-iso8601>
+ * © 2011 Colin Snover <http://zetafleet.com>
+ * Released under MIT license.
+ */
+
+Ember.Date = Ember.Date || {};
+
+var origParse = Date.parse, numericKeys = [ 1, 4, 5, 6, 7, 10, 11 ];
+Ember.Date.parse = function (date) {
+ var timestamp, struct, minutesOffset = 0;
+
+ // ES5 §15.9.4.2 states that the string should attempt to be parsed as a Date Time String Format string
+ // before falling back to any implementation-specific date parsing, so that’s what we do, even if native
+ // implementations could be faster
+ // 1 YYYY 2 MM 3 DD 4 HH 5 mm 6 ss 7 msec 8 Z 9 ± 10 tzHH 11 tzmm
+ if ((struct = /^(\d{4}|[+\-]\d{6})(?:-(\d{2})(?:-(\d{2}))?)?(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{3}))?)?(?:(Z)|([+\-])(\d{2})(?::(\d{2}))?)?)?$/.exec(date))) {
+ // avoid NaN timestamps caused by “undefined” values being passed to Date.UTC
+ for (var i = 0, k; (k = numericKeys[i]); ++i) {
+ struct[k] = +struct[k] || 0;
+ }
+
+ // allow undefined days and months
+ struct[2] = (+struct[2] || 1) - 1;
+ struct[3] = +struct[3] || 1;
+
+ if (struct[8] !== 'Z' && struct[9] !== undefined) {
+ minutesOffset = struct[10] * 60 + struct[11];
+
+ if (struct[9] === '+') {
+ minutesOffset = 0 - minutesOffset;
+ }
+ }
+
+ timestamp = Date.UTC(struct[1], struct[2], struct[3], struct[4], struct[5] + minutesOffset, struct[6], struct[7]);
+ }
+ else {
+ timestamp = origParse ? origParse(date) : NaN;
+ }
+
+ return timestamp;
+};
+
+if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Date) {
+ Date.parse = Ember.Date.parse;
+}
+
+})();
+
+
+
+(function() {
+
+})();
+
+
+
+(function() {
+var Evented = Ember.Evented, // ember-runtime/mixins/evented
+ Deferred = Ember.DeferredMixin, // ember-runtime/mixins/evented
+ run = Ember.run, // ember-metal/run-loop
+ get = Ember.get; // ember-metal/accessors
+
+var LoadPromise = Ember.Mixin.create(Evented, Deferred, {
+ init: function() {
+ this._super.apply(this, arguments);
+
+ this.one('didLoad', this, function() {
+ this.resolve(this);
+ });
+
+ this.one('becameError', this, function() {
+ this.reject(this);
+ });
+
+ if (get(this, 'isLoaded')) {
+ this.trigger('didLoad');
+ }
+ }
+});
+
+DS.LoadPromise = LoadPromise;
+
+})();
+
+
+
+(function() {
+/**
+*/
+
+var get = Ember.get, set = Ember.set;
+
+var LoadPromise = DS.LoadPromise; // system/mixins/load_promise
+
+/**
+ 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.
+
+ @module data
+ @submodule data-record-array
+ @main data-record-array
+
+ @class RecordArray
+ @namespace DS
+ @extends Ember.ArrayProxy
+ @uses Ember.Evented
+ @uses DS.LoadPromise
+*/
+
+DS.RecordArray = Ember.ArrayProxy.extend(LoadPromise, {
+ /**
+ The model type contained by this record array.
+
+ @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.
+ content: null,
+
+ isLoaded: false,
+ isUpdating: false,
+
+ // The store that created this record array.
+ store: null,
+
+ objectAtContent: function(index) {
+ var content = get(this, 'content'),
+ reference = content.objectAt(index),
+ store = get(this, 'store');
+
+ if (reference) {
+ return store.recordForReference(reference);
+ }
+ },
+
+ materializedObjectAt: function(index) {
+ var reference = get(this, 'content').objectAt(index);
+ if (!reference) { return; }
+
+ if (get(this, 'store').recordIsMaterialized(reference)) {
+ return this.objectAt(index);
+ }
+ },
+
+ update: function() {
+ if (get(this, 'isUpdating')) { return; }
+
+ var store = get(this, 'store'),
+ type = get(this, 'type');
+
+ store.fetchAll(type, this);
+ },
+
+ addReference: function(reference) {
+ get(this, 'content').addObject(reference);
+ },
+
+ removeReference: function(reference) {
+ get(this, 'content').removeObject(reference);
+ }
+});
+
+})();
+
+
+
+(function() {
+/**
+ @module data
+ @submodule data-record-array
+*/
+
+var get = Ember.get;
+
+/**
+ @class FilteredRecordArray
+ @namespace DS
+ @extends DS.RecordArray
+ @constructor
+*/
+DS.FilteredRecordArray = DS.RecordArray.extend({
+ 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.");
+ },
+
+ updateFilter: Ember.observer(function() {
+ var manager = get(this, 'manager');
+ manager.updateFilter(this, get(this, 'type'), get(this, 'filterFunction'));
+ }, 'filterFunction')
+});
+
+})();
+
+
+
+(function() {
+/**
+ @module data
+ @submodule data-record-array
+*/
+
+var get = Ember.get, set = Ember.set;
+
+/**
+ @class AdapterPopulatedRecordArray
+ @namespace DS
+ @extends DS.RecordArray
+ @constructor
+*/
+DS.AdapterPopulatedRecordArray = DS.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.");
+ },
+
+ load: function(references) {
+ this.setProperties({
+ content: Ember.A(references),
+ isLoaded: true
+ });
+
+ // TODO: does triggering didLoad event should be the last action of the runLoop?
+ Ember.run.once(this, 'trigger', 'didLoad');
+ }
+});
+
+})();
+
+
+
+(function() {
+/**
+ @module data
+ @submodule data-record-array
+*/
+
+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:
+
+ App.Post = DS.Model.extend({
+ comments: DS.hasMany('App.Comment')
+ });
+
+ App.Comment = DS.Model.extend({
+ post: DS.belongsTo('App.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
+ @constructor
+*/
+DS.ManyArray = DS.RecordArray.extend({
+ init: function() {
+ this._super.apply(this, arguments);
+ this._changesToSync = Ember.OrderedSet.create();
+ },
+
+ /**
+ @private
+
+ The record to which this relationship belongs.
+
+ @property {DS.Model}
+ */
+ owner: null,
+
+ /**
+ @private
+
+ `true` if the relationship is polymorphic, `false` otherwise.
+
+ @property {Boolean}
+ */
+ isPolymorphic: false,
+
+ // LOADING STATE
+
+ isLoaded: false,
+
+ loadingRecordsCount: function(count) {
+ this.loadingRecordsCount = count;
+ },
+
+ loadedRecord: function() {
+ this.loadingRecordsCount--;
+ if (this.loadingRecordsCount === 0) {
+ set(this, 'isLoaded', true);
+ this.trigger('didLoad');
+ }
+ },
+
+ fetch: function() {
+ var references = get(this, 'content'),
+ store = get(this, 'store'),
+ owner = get(this, 'owner');
+
+ store.fetchUnloadedReferences(references, owner);
+ },
+
+ // Overrides Ember.Array's replace method to implement
+ replaceContent: function(index, removed, added) {
+ // Map the array of record objects into an array of client ids.
+ added = added.map(function(record) {
+ Ember.assert("You can only add records of " + (get(this, 'type') && get(this, 'type').toString()) + " to this relationship.", !get(this, 'type') || (get(this, 'type').detectInstance(record)) );
+ return get(record, '_reference');
+ }, this);
+
+ this._super(index, removed, added);
+ },
+
+ arrangedContentDidChange: function() {
+ this.fetch();
+ },
+
+ arrayContentWillChange: function(index, removed, added) {
+ var owner = get(this, 'owner'),
+ name = get(this, 'name');
+
+ if (!owner._suspendedRelationships) {
+ // This code is the first half of code that continues inside
+ // of arrayContentDidChange. It gets or creates a change from
+ // the child object, adds the current owner as the old
+ // parent if this is the first time the object was removed
+ // from a ManyArray, and sets `newParent` to null.
+ //
+ // Later, if the object is added to another ManyArray,
+ // the `arrayContentDidChange` will set `newParent` on
+ // the change.
+ for (var i=index; i<index+removed; i++) {
+ var reference = get(this, 'content').objectAt(i);
+
+ var change = DS.RelationshipChange.createChange(owner.get('_reference'), reference, get(this, 'store'), {
+ parentType: owner.constructor,
+ changeType: "remove",
+ kind: "hasMany",
+ key: name
+ });
+
+ this._changesToSync.add(change);
+ }
+ }
+
+ return this._super.apply(this, arguments);
+ },
+
+ arrayContentDidChange: function(index, removed, added) {
+ this._super.apply(this, arguments);
+
+ var owner = get(this, 'owner'),
+ name = get(this, 'name'),
+ store = get(this, 'store');
+
+ if (!owner._suspendedRelationships) {
+ // This code is the second half of code that started in
+ // `arrayContentWillChange`. It gets or creates a change
+ // from the child object, and adds the current owner as
+ // the new parent.
+ for (var i=index; i<index+added; i++) {
+ var reference = get(this, 'content').objectAt(i);
+
+ var change = DS.RelationshipChange.createChange(owner.get('_reference'), reference, store, {
+ parentType: owner.constructor,
+ changeType: "add",
+ kind:"hasMany",
+ key: name
+ });
+ change.hasManyName = name;
+
+ this._changesToSync.add(change);
+ }
+
+ // We wait until the array has finished being
+ // mutated before syncing the OneToManyChanges created
+ // in arrayContentWillChange, so that the array
+ // membership test in the sync() logic operates
+ // on the final results.
+ this._changesToSync.forEach(function(change) {
+ change.sync();
+ });
+ DS.OneToManyChange.ensureSameTransaction(this._changesToSync, store);
+ this._changesToSync.clear();
+ }
+ },
+
+ // Create a child record within the owner
+ createRecord: function(hash, transaction) {
+ var owner = get(this, 'owner'),
+ store = get(owner, 'store'),
+ type = get(this, 'type'),
+ record;
+
+ Ember.assert("You can not create records of " + (get(this, 'type') && get(this, 'type').toString()) + " on this polymorphic relationship.", !get(this, 'isPolymorphic'));
+
+ transaction = transaction || get(owner, 'transaction');
+
+ record = store.createRecord.call(store, type, hash, transaction);
+ this.pushObject(record);
+
+ return record;
+ }
+
+});
+
+})();
+
+
+
+(function() {
+/**
+ @module data
+ @submodule data-record-array
+*/
+
+})();
+
+
+
+(function() {
+var get = Ember.get, set = Ember.set, forEach = Ember.EnumerableUtils.forEach;
+
+/**
+ @module data
+ @submodule data-transaction
+*/
+
+/**
+ A transaction allows you to collect multiple records into a unit of work
+ that can be committed or rolled back as a group.
+
+ For example, if a record has local modifications that have not yet
+ been saved, calling `commit()` on its transaction will cause those
+ modifications to be sent to the adapter to be saved. Calling
+ `rollback()` on its transaction would cause all of the modifications to
+ be discarded and the record to return to the last known state before
+ changes were made.
+
+ If a newly created record's transaction is rolled back, it will
+ immediately transition to the deleted state.
+
+ If you do not explicitly create a transaction, a record is assigned to
+ an implicit transaction called the default transaction. In these cases,
+ you can treat your application's instance of `DS.Store` as a transaction
+ and call the `commit()` and `rollback()` methods on the store itself.
+
+ Once a record has been successfully committed or rolled back, it will
+ be moved back to the implicit transaction. Because it will now be in
+ a clean state, it can be moved to a new transaction if you wish.
+
+ ### Creating a Transaction
+
+ To create a new transaction, call the `transaction()` method of your
+ application's `DS.Store` instance:
+
+ var transaction = App.store.transaction();
+
+ This will return a new instance of `DS.Transaction` with no records
+ yet assigned to it.
+
+ ### Adding Existing Records
+
+ Add records to a transaction using the `add()` method:
+
+ record = App.store.find(App.Person, 1);
+ transaction.add(record);
+
+ Note that only records whose `isDirty` flag is `false` may be added
+ to a transaction. Once modifications to a record have been made
+ (its `isDirty` flag is `true`), it is not longer able to be added to
+ a transaction.
+
+ ### Creating New Records
+
+ Because newly created records are dirty from the time they are created,
+ and because dirty records can not be added to a transaction, you must
+ use the `createRecord()` method to assign new records to a transaction.
+
+ For example, instead of this:
+
+ var transaction = store.transaction();
+ var person = App.Person.createRecord({ name: "Steve" });
+
+ // won't work because person is dirty
+ transaction.add(person);
+
+ Call `createRecord()` on the transaction directly:
+
+ var transaction = store.transaction();
+ transaction.createRecord(App.Person, { name: "Steve" });
+
+ ### Asynchronous Commits
+
+ Typically, all of the records in a transaction will be committed
+ together. However, new records that have a dependency on other new
+ records need to wait for their parent record to be saved and assigned an
+ ID. In that case, the child record will continue to live in the
+ transaction until its parent is saved, at which time the transaction will
+ attempt to commit again.
+
+ For this reason, you should not re-use transactions once you have committed
+ them. Always make a new transaction and move the desired records to it before
+ calling commit.
+*/
+
+DS.Transaction = Ember.Object.extend({
+ /**
+ @private
+
+ Creates the bucket data structure used to segregate records by
+ type.
+ */
+ init: function() {
+ set(this, 'records', Ember.OrderedSet.create());
+ },
+
+ /**
+ Creates a new record of the given type and assigns it to the transaction
+ on which the method was called.
+
+ This is useful as only clean records can be added to a transaction and
+ new records created using other methods immediately become dirty.
+
+ @param {DS.Model} type the model type to create
+ @param {Object} hash the data hash to assign the new record
+ */
+ createRecord: function(type, hash) {
+ var store = get(this, 'store');
+
+ return store.createRecord(type, hash, this);
+ },
+
+ isEqualOrDefault: function(other) {
+ if (this === other || other === get(this, 'store.defaultTransaction')) {
+ return true;
+ }
+ },
+
+ isDefault: Ember.computed(function() {
+ return this === get(this, 'store.defaultTransaction');
+ }).volatile(),
+
+ /**
+ Adds an existing record to this transaction. Only records without
+ modificiations (i.e., records whose `isDirty` property is `false`)
+ can be added to a transaction.
+
+ @param {DS.Model} record the record to add to the transaction
+ */
+ add: function(record) {
+ Ember.assert("You must pass a record into transaction.add()", record instanceof DS.Model);
+
+ var store = get(this, 'store');
+ var adapter = get(store, '_adapter');
+ var serializer = get(adapter, 'serializer');
+ serializer.eachEmbeddedRecord(record, function(embeddedRecord, embeddedType) {
+ if (embeddedType === 'load') { return; }
+
+ this.add(embeddedRecord);
+ }, this);
+
+ this.adoptRecord(record);
+ },
+
+ relationships: Ember.computed(function() {
+ var relationships = Ember.OrderedSet.create(),
+ records = get(this, 'records'),
+ store = get(this, 'store');
+
+ records.forEach(function(record) {
+ var reference = get(record, '_reference');
+ var changes = store.relationshipChangesFor(reference);
+ for(var i = 0; i < changes.length; i++) {
+ relationships.add(changes[i]);
+ }
+ });
+
+ return relationships;
+ }).volatile(),
+
+ commitDetails: Ember.computed(function() {
+ var commitDetails = Ember.MapWithDefault.create({
+ defaultValue: function() {
+ return {
+ created: Ember.OrderedSet.create(),
+ updated: Ember.OrderedSet.create(),
+ deleted: Ember.OrderedSet.create()
+ };
+ }
+ });
+
+ var records = get(this, 'records'),
+ store = get(this, 'store');
+
+ records.forEach(function(record) {
+ if(!get(record, 'isDirty')) return;
+ record.send('willCommit');
+ var adapter = store.adapterForType(record.constructor);
+ commitDetails.get(adapter)[get(record, 'dirtyType')].add(record);
+ });
+
+ return commitDetails;
+ }).volatile(),
+
+ /**
+ Commits the transaction, which causes all of the modified records that
+ belong to the transaction to be sent to the adapter to be saved.
+
+ Once you call `commit()` on a transaction, you should not re-use it.
+
+ When a record is saved, it will be removed from this transaction and
+ moved back to the store's default transaction.
+ */
+ commit: function() {
+ var store = get(this, 'store');
+
+ if (get(this, 'isDefault')) {
+ set(store, 'defaultTransaction', store.transaction());
+ }
+
+ this.removeCleanRecords();
+
+ var commitDetails = get(this, 'commitDetails'),
+ relationships = get(this, 'relationships');
+
+ commitDetails.forEach(function(adapter, commitDetails) {
+ Ember.assert("You tried to commit records but you have no adapter", adapter);
+ Ember.assert("You tried to commit records but your adapter does not implement `commit`", adapter.commit);
+
+ adapter.commit(store, commitDetails);
+ });
+
+ // Once we've committed the transaction, there is no need to
+ // keep the OneToManyChanges around. Destroy them so they
+ // can be garbage collected.
+ relationships.forEach(function(relationship) {
+ relationship.destroy();
+ });
+ },
+
+ /**
+ Rolling back a transaction resets the records that belong to
+ that transaction.
+
+ Updated records have their properties reset to the last known
+ value from the persistence layer. Deleted records are reverted
+ to a clean, non-deleted state. Newly created records immediately
+ become deleted, and are not sent to the adapter to be persisted.
+
+ After the transaction is rolled back, any records that belong
+ to it will return to the store's default transaction, and the
+ current transaction should not be used again.
+ */
+ rollback: function() {
+ // Destroy all relationship changes and compute
+ // all references affected
+ var references = Ember.OrderedSet.create();
+ var relationships = get(this, 'relationships');
+ relationships.forEach(function(r) {
+ references.add(r.firstRecordReference);
+ references.add(r.secondRecordReference);
+ r.destroy();
+ });
+
+ var records = get(this, 'records');
+ records.forEach(function(record) {
+ if (!record.get('isDirty')) return;
+ record.send('rollback');
+ });
+
+ // Now that all records in the transaction are guaranteed to be
+ // clean, migrate them all to the store's default transaction.
+ this.removeCleanRecords();
+
+ // Remaining associated references are not part of the transaction, but
+ // can still have hasMany's which have not been reloaded
+ references.forEach(function(r) {
+ if (r && r.record) {
+ var record = r.record;
+ record.suspendRelationshipObservers(function() {
+ record.reloadHasManys();
+ });
+ }
+ }, this);
+ },
+
+ /**
+ @private
+
+ Removes a record from this transaction and back to the store's
+ default transaction.
+
+ Note: This method is private for now, but should probably be exposed
+ in the future once we have stricter error checking (for example, in the
+ case of the record being dirty).
+
+ @param {DS.Model} record
+ */
+ remove: function(record) {
+ var defaultTransaction = get(this, 'store.defaultTransaction');
+ defaultTransaction.adoptRecord(record);
+ },
+
+ /**
+ @private
+
+ Removes all of the records in the transaction's clean bucket.
+ */
+ removeCleanRecords: function() {
+ var records = get(this, 'records');
+ records.forEach(function(record) {
+ if(!record.get('isDirty')) {
+ this.remove(record);
+ }
+ }, this);
+ },
+
+ /**
+ @private
+
+ This method moves a record into a different transaction without the normal
+ checks that ensure that the user is not doing something weird, like moving
+ a dirty record into a new transaction.
+
+ It is designed for internal use, such as when we are moving a clean record
+ into a new transaction when the transaction is committed.
+
+ This method must not be called unless the record is clean.
+
+ @param {DS.Model} record
+ */
+ adoptRecord: function(record) {
+ var oldTransaction = get(record, 'transaction');
+
+ if (oldTransaction) {
+ oldTransaction.removeRecord(record);
+ }
+
+ get(this, 'records').add(record);
+ set(record, 'transaction', this);
+ },
+
+ /**
+ @private
+
+ Removes the record without performing the normal checks
+ to ensure that the record is re-added to the store's
+ default transaction.
+ */
+ removeRecord: function(record) {
+ get(this, 'records').remove(record);
+ }
+
+});
+
+DS.Transaction.reopenClass({
+ ensureSameTransaction: function(records){
+ var transactions = Ember.A();
+ forEach( records, function(record){
+ if (record){ transactions.pushObject(get(record, 'transaction')); }
+ });
+
+ var transaction = transactions.reduce(function(prev, t) {
+ if (!get(t, 'isDefault')) {
+ if (prev === null) { return t; }
+ Ember.assert("All records in a changed relationship must be in the same transaction. You tried to change the relationship between records when one is in " + t + " and the other is in " + prev, t === prev);
+ }
+
+ return prev;
+ }, null);
+
+ if (transaction) {
+ forEach( records, function(record){
+ if (record){ transaction.add(record); }
+ });
+ } else {
+ transaction = transactions.objectAt(0);
+ }
+ return transaction;
+ }
+});
+
+})();
+
+
+
+(function() {
+var get = Ember.get;
+
+/**
+ The Mappable mixin is designed for classes that would like to
+ behave as a map for configuration purposes.
+
+ For example, the DS.Adapter class can behave like a map, with
+ more semantic API, via the `map` API:
+
+ DS.Adapter.map('App.Person', { firstName: { key: 'FIRST' } });
+
+ Class configuration via a map-like API has a few common requirements
+ that differentiate it from the standard Ember.Map implementation.
+
+ First, values often are provided as strings that should be normalized
+ into classes the first time the configuration options are used.
+
+ Second, the values configured on parent classes should also be taken
+ into account.
+
+ Finally, setting the value of a key sometimes should merge with the
+ previous value, rather than replacing it.
+
+ This mixin provides a instance method, `createInstanceMapFor`, that
+ will reify all of the configuration options set on an instance's
+ constructor and provide it for the instance to use.
+
+ Classes can implement certain hooks that allow them to customize
+ the requirements listed above:
+
+ * `resolveMapConflict` - called when a value is set for an existing
+ value
+ * `transformMapKey` - allows a key name (for example, a global path
+ to a class) to be normalized
+ * `transformMapValue` - allows a value (for example, a class that
+ should be instantiated) to be normalized
+
+ Classes that implement this mixin should also implement a class
+ method built using the `generateMapFunctionFor` method:
+
+ DS.Adapter.reopenClass({
+ map: DS.Mappable.generateMapFunctionFor('attributes', function(key, newValue, map) {
+ var existingValue = map.get(key);
+
+ for (var prop in newValue) {
+ if (!newValue.hasOwnProperty(prop)) { continue; }
+ existingValue[prop] = newValue[prop];
+ }
+ })
+ });
+
+ The function passed to `generateMapFunctionFor` is invoked every time a
+ new value is added to the map.
+
+ @class _Mappable
+ @private
+ @namespace DS
+ @extends Ember.Mixin
+**/
+
+var resolveMapConflict = function(oldValue, newValue) {
+ return oldValue;
+};
+
+var transformMapKey = function(key, value) {
+ return key;
+};
+
+var transformMapValue = function(key, value) {
+ return value;
+};
+
+DS._Mappable = Ember.Mixin.create({
+ createInstanceMapFor: function(mapName) {
+ var instanceMeta = getMappableMeta(this);
+
+ instanceMeta.values = instanceMeta.values || {};
+
+ if (instanceMeta.values[mapName]) { return instanceMeta.values[mapName]; }
+
+ var instanceMap = instanceMeta.values[mapName] = new Ember.Map();
+
+ var klass = this.constructor;
+
+ while (klass && klass !== DS.Store) {
+ this._copyMap(mapName, klass, instanceMap);
+ klass = klass.superclass;
+ }
+
+ instanceMeta.values[mapName] = instanceMap;
+ return instanceMap;
+ },
+
+ _copyMap: function(mapName, klass, instanceMap) {
+ var classMeta = getMappableMeta(klass);
+
+ var classMap = classMeta[mapName];
+ if (classMap) {
+ classMap.forEach(eachMap, this);
+ }
+
+ function eachMap(key, value) {
+ var transformedKey = (klass.transformMapKey || transformMapKey)(key, value);
+ var transformedValue = (klass.transformMapValue || transformMapValue)(key, value);
+
+ var oldValue = instanceMap.get(transformedKey);
+ var newValue = transformedValue;
+
+ if (oldValue) {
+ newValue = (this.constructor.resolveMapConflict || resolveMapConflict)(oldValue, newValue);
+ }
+
+ instanceMap.set(transformedKey, newValue);
+ }
+ }
+
+
+});
+
+DS._Mappable.generateMapFunctionFor = function(mapName, transform) {
+ return function(key, value) {
+ var meta = getMappableMeta(this);
+
+ var map = meta[mapName] || Ember.MapWithDefault.create({
+ defaultValue: function() { return {}; }
+ });
+
+ transform.call(this, key, value, map);
+
+ meta[mapName] = map;
+ };
+};
+
+function getMappableMeta(obj) {
+ var meta = Ember.meta(obj, true),
+ keyName = 'DS.Mappable',
+ value = meta[keyName];
+
+ if (!value) { meta[keyName] = {}; }
+
+ if (!meta.hasOwnProperty(keyName)) {
+ meta[keyName] = Ember.create(meta[keyName]);
+ }
+
+ return meta[keyName];
+}
+
+})();
+
+
+
+(function() {
+/*globals Ember*/
+/*jshint eqnull:true*/
+/**
+ @module data
+ @submodule data-store
+*/
+
+var get = Ember.get, set = Ember.set;
+var once = Ember.run.once;
+var isNone = Ember.isNone;
+var forEach = Ember.EnumerableUtils.forEach;
+var map = Ember.EnumerableUtils.map;
+
+// These values are used in the data cache when clientIds are
+// needed but the underlying data has not yet been loaded by
+// the server.
+var UNLOADED = 'unloaded';
+var LOADING = 'loading';
+var MATERIALIZED = { materialized: true };
+var CREATED = { created: true };
+
+// 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.
+var coerceId = function(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:
+
+ 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.Model`'s `find()` method:
+
+ var person = App.Person.find(123);
+
+ If your application has multiple `DS.Store` instances (an unusual case), you can
+ specify which store should be used:
+
+ var person = store.find(App.Person, 123);
+
+ In general, you should retrieve models using the methods on `DS.Model`; you should
+ rarely need to interact with the store directly.
+
+ 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:
+
+ MyApp.store = DS.Store.create({
+ adapter: 'MyApp.CustomAdapter'
+ });
+
+ You can learn more about writing a custom adapter by reading the `DS.Adapter`
+ documentation.
+
+ @class Store
+ @namespace DS
+ @extends Ember.Object
+ @uses DS._Mappable
+ @constructor
+*/
+DS.Store = Ember.Object.extend(DS._Mappable, {
+
+ /**
+ Many methods can be invoked without specifying which store should be used.
+ In those cases, the first store created will be used as the default. If
+ an application has multiple stores, it should specify which store to use
+ when performing actions, such as finding records by ID.
+
+ The init method registers this store as the default if none is specified.
+ */
+ init: function() {
+ if (!get(DS, 'defaultStore') || get(this, 'isDefaultStore')) {
+ set(DS, 'defaultStore', this);
+ }
+
+ // internal bookkeeping; not observable
+ this.typeMaps = {};
+ this.recordArrayManager = DS.RecordArrayManager.create({
+ store: this
+ });
+ this.relationshipChanges = {};
+
+ set(this, 'currentTransaction', this.transaction());
+ set(this, 'defaultTransaction', this.transaction());
+ },
+
+ /**
+ Returns a new transaction scoped to this store. This delegates
+ responsibility for invoking the adapter's commit mechanism to
+ a transaction.
+
+ Transaction are responsible for tracking changes to records
+ added to them, and supporting `commit` and `rollback`
+ functionality. Committing a transaction invokes the store's
+ adapter, while rolling back a transaction reverses all
+ changes made to records added to the transaction.
+
+ A store has an implicit (default) transaction, which tracks changes
+ made to records not explicitly added to a transaction.
+
+ @see {DS.Transaction}
+ @returns DS.Transaction
+ */
+ transaction: function() {
+ return DS.Transaction.create({ store: this });
+ },
+
+ /**
+ @private
+
+ Instructs the store to materialize the data for a given record.
+
+ To materialize a record, the store first retrieves the opaque data that was
+ passed to either `load()` or `loadMany()`. Then, the data and the record
+ are passed to the adapter's `materialize()` method, which allows the adapter
+ to translate arbitrary data structures from the adapter into the normalized
+ form the record expects.
+
+ The adapter's `materialize()` method will invoke `materializeAttribute()`,
+ `materializeHasMany()` and `materializeBelongsTo()` on the record to
+ populate it with normalized values.
+
+ @param {DS.Model} record
+ */
+ materializeData: function(record) {
+ var reference = get(record, '_reference'),
+ data = reference.data,
+ adapter = this.adapterForType(record.constructor);
+
+ reference.data = MATERIALIZED;
+
+ record.setupData();
+
+ if (data !== CREATED) {
+ // Instructs the adapter to extract information from the
+ // opaque data and materialize the record's attributes and
+ // relationships.
+ adapter.materialize(record, data, reference.prematerialized);
+ }
+ },
+
+ /**
+ The adapter to use to communicate to a backend server or other persistence layer.
+
+ This can be specified as an instance, a class, or a property path that specifies
+ where the adapter can be located.
+
+ @property {DS.Adapter|String}
+ */
+ adapter: Ember.computed(function(){
+ if (!Ember.testing) {
+ Ember.debug("A custom DS.Adapter was not provided as the 'Adapter' property of your application's Store. The default (DS.RESTAdapter) will be used.");
+ }
+
+ return 'DS.RESTAdapter';
+ }).property(),
+
+
+ /**
+ @private
+
+ Returns a JSON representation of the record using the adapter's
+ serialization strategy. This method exists primarily to enable
+ a record, which has access to its store (but not the store's
+ adapter) to provide a `serialize()` convenience.
+
+ The available options are:
+
+ * `includeId`: `true` if the record's ID should be included in
+ the JSON representation
+
+ @param {DS.Model} record the record to serialize
+ @param {Object} options an options hash
+ */
+ serialize: function(record, options) {
+ return this.adapterForType(record.constructor).serialize(record, options);
+ },
+
+ /**
+ @private
+
+ This property returns the adapter, after resolving a possible
+ property path.
+
+ 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.
+
+ @returns DS.Adapter
+ */
+ _adapter: Ember.computed(function() {
+ var adapter = get(this, 'adapter');
+ if (typeof adapter === 'string') {
+ adapter = get(this, adapter, false) || get(Ember.lookup, adapter);
+ }
+
+ if (DS.Adapter.detect(adapter)) {
+ adapter = adapter.create();
+ }
+
+ return adapter;
+ }).property('adapter'),
+
+ /**
+ @private
+
+ A monotonically increasing number to be used to uniquely identify
+ data and records.
+
+ It starts at 1 so other parts of the code can test for truthiness
+ when provided a `clientId` instead of having to explicitly test
+ for undefined.
+ */
+ clientIdCounter: 1,
+
+ // .....................
+ // . CREATE NEW RECORD .
+ // .....................
+
+ /**
+ Create a new record in the current store. The properties passed
+ to this method are set on the newly created record.
+
+ Note: The third `transaction` property is for internal use only.
+ If you want to create a record inside of a given transaction,
+ use `transaction.createRecord()` instead of `store.createRecord()`.
+
+ @method createRecord
+ @param {subclass of DS.Model} type
+ @param {Object} properties a hash of properties to set on the
+ newly created record.
+ @returns DS.Model
+ */
+ createRecord: function(type, properties, transaction) {
+ properties = properties || {};
+
+ // Create a new instance of the model `type` and put it
+ // into the specified `transaction`. If no transaction is
+ // specified, the default transaction will be used.
+ var record = type._create({
+ store: this
+ });
+
+ transaction = transaction || get(this, 'defaultTransaction');
+
+ // adoptRecord is an internal API that allows records to move
+ // into a transaction without assertions designed for app
+ // code. It is used here to ensure that regardless of new
+ // restrictions on the use of the public `transaction.add()`
+ // API, we will always be able to insert new records into
+ // their transaction.
+ transaction.adoptRecord(record);
+
+ // `id` is a special property that may not be a `DS.attr`
+ var id = properties.id;
+
+ // 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(id)) {
+ var adapter = this.adapterForType(type);
+
+ if (adapter && adapter.generateIdForRecord) {
+ id = coerceId(adapter.generateIdForRecord(this, record));
+ properties.id = id;
+ }
+ }
+
+ // Coerce ID to a string
+ id = coerceId(id);
+
+ // Create a new `clientId` and associate it with the
+ // specified (or generated) `id`. Since we don't have
+ // any data for the server yet (by definition), store
+ // the sentinel value CREATED as the data for this
+ // clientId. If we see this value later, we will skip
+ // materialization.
+ var reference = this.createReference(type, id);
+ reference.data = CREATED;
+
+ // Now that we have a reference, attach it to the record we
+ // just created.
+ set(record, '_reference', reference);
+ reference.record = record;
+
+ // Move the record out of its initial `empty` state into
+ // the `loaded` state.
+ record.loadedData();
+
+ record.setupData();
+
+ // Set the properties specified on the record.
+ record.setProperties(properties);
+
+ // Resolve record promise
+ Ember.run(record, 'resolve', record);
+
+ return record;
+ },
+
+ // .................
+ // . DELETE RECORD .
+ // .................
+
+ /**
+ For symmetry, a record can be deleted via the store.
+
+ @param {DS.Model} record
+ */
+ deleteRecord: function(record) {
+ record.deleteRecord();
+ },
+
+ /**
+ For symmetry, a record can be unloaded via the store.
+
+ @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 always a subclass of `DS.Model`.
+
+ You can use the `find` method on a subclass of `DS.Model` directly if your
+ application only has one store. For example, instead of
+ `store.find(App.Person, 1)`, you could say `App.Person.find(1)`.
+
+ ---
+
+ To find a record by ID, pass the `id` as the second parameter:
+
+ store.find(App.Person, 1);
+ App.Person.find(1);
+
+ If the record with that `id` had not previously been loaded, the store will
+ return an empty record immediately and ask the adapter to find the data by
+ calling the adapter's `find` method.
+
+ The `find` method will always return the same object for a given type and
+ `id`. To check whether the adapter has populated a record, you can check
+ its `isLoaded` property.
+
+ ---
+
+ To find all records for a type, call `find` with no additional parameters:
+
+ store.find(App.Person);
+ App.Person.find();
+
+ This will return a `RecordArray` representing all known records for the
+ given type and kick off a request to the adapter's `findAll` method to load
+ any additional records for the type.
+
+ The `RecordArray` returned by `find()` is live. If any more records for the
+ type are added at a later time through any mechanism, it will automatically
+ update to reflect the change.
+
+ ---
+
+ To find a record by a query, call `find` with a hash as the second
+ parameter:
+
+ store.find(App.Person, { page: 1 });
+ App.Person.find({ page: 1 });
+
+ This will return a `RecordArray` immediately, but it will always be an
+ empty `RecordArray` at first. It will call the adapter's `findQuery`
+ method, which will populate the `RecordArray` once the server has returned
+ results.
+
+ You can check whether a query results `RecordArray` has loaded by checking
+ its `isLoaded` property.
+
+ @method find
+ @param {DS.Model} type
+ @param {Object|String|Integer|null} id
+ */
+ find: function(type, id) {
+ if (id === undefined) {
+ return this.findAll(type);
+ }
+
+ // We are passed a query instead of an id.
+ if (Ember.typeOf(id) === 'object') {
+ return this.findQuery(type, id);
+ }
+
+ return this.findById(type, coerceId(id));
+ },
+
+ /**
+ @private
+
+ This method returns a record for a given type and id combination.
+
+ If the store has never seen this combination of type and id before, it
+ creates a new `clientId` with the LOADING sentinel and asks the adapter to
+ load the data.
+
+ If the store has seen the combination, this method delegates to
+ `getByReference`.
+ */
+ findById: function(type, id) {
+ var reference;
+
+ if (this.hasReferenceForId(type, id)) {
+ reference = this.referenceForId(type, id);
+
+ if (reference.data !== UNLOADED) {
+ return this.recordForReference(reference);
+ }
+ }
+
+ if (!reference) {
+ reference = this.createReference(type, id);
+ }
+
+ reference.data = LOADING;
+
+ // create a new instance of the model type in the
+ // 'isLoading' state
+ var record = this.materializeRecord(reference);
+
+ if (reference.data === LOADING) {
+ // let the adapter set the data, possibly async
+ var adapter = this.adapterForType(type),
+ store = this;
+
+ Ember.assert("You tried to find a record but you have no adapter (for " + type + ")", adapter);
+ Ember.assert("You tried to find a record but your adapter does not implement `find`", adapter.find);
+
+ var thenable = adapter.find(this, type, id);
+
+ if (thenable && thenable.then) {
+ thenable.then(null /* for future use */, function(error) {
+ store.recordWasError(record);
+ });
+ }
+ }
+
+ return record;
+ },
+
+ reloadRecord: function(record) {
+ var type = record.constructor,
+ adapter = this.adapterForType(type),
+ store = this,
+ id = get(record, 'id');
+
+ Ember.assert("You cannot update a record without an ID", id);
+ Ember.assert("You tried to update a record but you have no adapter (for " + type + ")", adapter);
+ Ember.assert("You tried to update a record but your adapter does not implement `find`", adapter.find);
+
+ var thenable = adapter.find(this, type, id);
+
+ if (thenable && thenable.then) {
+ thenable.then(null /* for future use */, function(error) {
+ store.recordWasError(record);
+ });
+ }
+ },
+
+ /**
+ @private
+
+ This method returns a record for a given record refeence.
+
+ If no record for the reference has yet been materialized, this method will
+ materialize a new `DS.Model` instance. This allows adapters to eagerly load
+ large amounts of data into the store, and avoid incurring the cost of
+ creating models until they are requested.
+
+ In short, it's a convenient way to get a record for a known
+ record reference, materializing it if necessary.
+
+ @param {Object} reference
+ @returns {DS.Model}
+ */
+ recordForReference: function(reference) {
+ var record = reference.record;
+
+ if (!record) {
+ // create a new instance of the model type in the
+ // 'isLoading' state
+ record = this.materializeRecord(reference);
+ }
+
+ return record;
+ },
+
+ /**
+ @private
+
+ Given an array of `reference`s, determines which of those
+ `clientId`s has not yet been loaded.
+
+ In preparation for loading, this method also marks any unloaded
+ `clientId`s as loading.
+ */
+ unloadedReferences: function(references) {
+ var unloadedReferences = [];
+
+ for (var i=0, l=references.length; i<l; i++) {
+ var reference = references[i];
+
+ if (reference.data === UNLOADED) {
+ unloadedReferences.push(reference);
+ reference.data = LOADING;
+ }
+ }
+
+ return unloadedReferences;
+ },
+
+ /**
+ @private
+
+ This method is the entry point that relationships use to update
+ themselves when their underlying data changes.
+
+ First, it determines which of its `reference`s are still unloaded,
+ then invokes `findMany` on the adapter.
+ */
+ fetchUnloadedReferences: function(references, owner) {
+ var unloadedReferences = this.unloadedReferences(references);
+ this.fetchMany(unloadedReferences, owner);
+ },
+
+ /**
+ @private
+
+ This method takes a list of `reference`s, groups the `reference`s by type,
+ converts the `reference`s into IDs, and then invokes the adapter's `findMany`
+ method.
+
+ The `reference`s are grouped by type to invoke `findMany` on adapters
+ for each unique type in `reference`s.
+
+ It is used both by a brand new relationship (via the `findMany`
+ method) or when the data underlying an existing relationship
+ changes (via the `fetchUnloadedReferences` method).
+ */
+ fetchMany: function(references, owner) {
+ if (!references.length) { return; }
+
+ // Group By Type
+ var referencesByTypeMap = Ember.MapWithDefault.create({
+ defaultValue: function() { return Ember.A(); }
+ });
+ forEach(references, function(reference) {
+ referencesByTypeMap.get(reference.type).push(reference);
+ });
+
+ forEach(referencesByTypeMap, function(type) {
+ var references = referencesByTypeMap.get(type),
+ ids = map(references, function(reference) { return reference.id; });
+
+ var adapter = this.adapterForType(type);
+
+ Ember.assert("You tried to load many records but you have no adapter (for " + type + ")", adapter);
+ Ember.assert("You tried to load many records but your adapter does not implement `findMany`", adapter.findMany);
+
+ adapter.findMany(this, type, ids, owner);
+ }, this);
+ },
+
+ hasReferenceForId: function(type, id) {
+ id = coerceId(id);
+
+ return !!this.typeMapFor(type).idToReference[id];
+ },
+
+ referenceForId: function(type, id) {
+ id = coerceId(id);
+
+ // Check to see if we have seen this type/id pair before.
+ var reference = this.typeMapFor(type).idToReference[id];
+
+ // If not, create a reference for it but don't populate it
+ // with any data yet.
+ if (!reference) {
+ reference = this.createReference(type, id);
+ reference.data = UNLOADED;
+ }
+
+ return reference;
+ },
+
+ /**
+ @private
+
+ `findMany` is the entry point that relationships use to generate a
+ new `ManyArray` for the list of IDs specified by the server for
+ the relationship.
+
+ Its responsibilities are:
+
+ * convert the IDs into clientIds
+ * determine which of the clientIds still need to be loaded
+ * create a new ManyArray whose content is *all* of the clientIds
+ * notify the ManyArray of the number of its elements that are
+ already loaded
+ * insert the unloaded references into the `loadingRecordArrays`
+ bookkeeping structure, which will allow the `ManyArray` to know
+ when all of its loading elements are loaded from the server.
+ * ask the adapter to load the unloaded elements, by invoking
+ findMany with the still-unloaded IDs.
+ */
+ findMany: function(type, idsOrReferencesOrOpaque, record, relationship) {
+ // 1. Determine which of the client ids need to be loaded
+ // 2. Create a new ManyArray whose content is ALL of the clientIds
+ // 3. Decrement the ManyArray's counter by the number of loaded clientIds
+ // 4. Put the ManyArray into our bookkeeping data structure, keyed on
+ // the needed clientIds
+ // 5. Ask the adapter to load the records for the unloaded clientIds (but
+ // convert them back to ids)
+
+ if (!Ember.isArray(idsOrReferencesOrOpaque)) {
+ var adapter = this.adapterForType(type);
+
+ if (adapter && adapter.findHasMany) {
+ adapter.findHasMany(this, record, relationship, idsOrReferencesOrOpaque);
+ } else if (idsOrReferencesOrOpaque !== undefined) {
+ Ember.assert("You tried to load many records but you have no adapter (for " + type + ")", adapter);
+ Ember.assert("You tried to load many records but your adapter does not implement `findHasMany`", adapter.findHasMany);
+ }
+
+ return this.recordArrayManager.createManyArray(type, Ember.A());
+ }
+
+ // Coerce server IDs into Record Reference
+ var references = map(idsOrReferencesOrOpaque, function(reference) {
+ if (typeof reference !== 'object' && reference !== null) {
+ return this.referenceForId(type, reference);
+ }
+
+ return reference;
+ }, this);
+
+ var unloadedReferences = this.unloadedReferences(references),
+ manyArray = this.recordArrayManager.createManyArray(type, Ember.A(references)),
+ reference, i, l;
+
+ // Start the decrementing counter on the ManyArray at the number of
+ // records we need to load from the adapter
+ manyArray.loadingRecordsCount(unloadedReferences.length);
+
+ if (unloadedReferences.length) {
+ for (i=0, l=unloadedReferences.length; i<l; i++) {
+ reference = unloadedReferences[i];
+
+ // keep track of the record arrays that a given loading record
+ // is part of. This way, if the same record is in multiple
+ // ManyArrays, all of their loading records counters will be
+ // decremented when the adapter provides the data.
+ this.recordArrayManager.registerWaitingRecordArray(manyArray, reference);
+ }
+
+ this.fetchMany(unloadedReferences, record);
+ } else {
+ // all requested records are available
+ manyArray.set('isLoaded', true);
+
+ Ember.run.once(function() {
+ manyArray.trigger('didLoad');
+ });
+ }
+
+ return manyArray;
+ },
+
+ /**
+ This method delegates a query to the adapter. This is the one place where
+ adapter-level semantics are exposed to the application.
+
+ Exposing queries this way seems preferable to creating an abstract query
+ language for all server-side queries, and then require all adapters to
+ implement them.
+
+ @private
+ @method findQuery
+ @param {Class} type
+ @param {Object} query an opaque query to be used by the adapter
+ @return {DS.AdapterPopulatedRecordArray}
+ */
+ findQuery: function(type, query) {
+ var array = DS.AdapterPopulatedRecordArray.create({ type: type, query: query, content: Ember.A([]), store: this });
+ var adapter = this.adapterForType(type);
+
+ Ember.assert("You tried to load a query but you have no adapter (for " + type + ")", adapter);
+ Ember.assert("You tried to load a query but your adapter does not implement `findQuery`", adapter.findQuery);
+
+ adapter.findQuery(this, type, query, array);
+
+ return array;
+ },
+
+ /**
+ @private
+
+ This method returns an array of all records adapter can find.
+ It triggers the adapter's `findAll` method to give it an opportunity to populate
+ the array with records of that type.
+
+ @param {Class} type
+ @return {DS.AdapterPopulatedRecordArray}
+ */
+ findAll: function(type) {
+ return this.fetchAll(type, this.all(type));
+ },
+
+ /**
+ @private
+ */
+ fetchAll: function(type, array) {
+ var adapter = this.adapterForType(type),
+ sinceToken = this.typeMapFor(type).metadata.since;
+
+ set(array, 'isUpdating', true);
+
+ Ember.assert("You tried to load all records but you have no adapter (for " + type + ")", adapter);
+ Ember.assert("You tried to load all records but your adapter does not implement `findAll`", adapter.findAll);
+
+ adapter.findAll(this, type, sinceToken);
+
+ return array;
+ },
+
+ /**
+ */
+ metaForType: function(type, property, data) {
+ var target = this.typeMapFor(type).metadata;
+ set(target, property, data);
+ },
+
+ /**
+ */
+ didUpdateAll: function(type) {
+ var findAllCache = this.typeMapFor(type).findAllCache;
+ set(findAllCache, 'isUpdating', false);
+ },
+
+ /**
+ This method returns a filtered array that contains all of the known records
+ for a given type.
+
+ Note that because it's just a filter, it will have any locally
+ created records of the type.
+
+ Also note that multiple calls to `all` for a given type will always
+ return the same RecordArray.
+
+ @method all
+ @param {Class} type
+ @return {DS.RecordArray}
+ */
+ all: function(type) {
+ var typeMap = this.typeMapFor(type),
+ findAllCache = typeMap.findAllCache;
+
+ if (findAllCache) { return findAllCache; }
+
+ var array = DS.RecordArray.create({
+ type: type,
+ content: Ember.A([]),
+ store: this,
+ isLoaded: true
+ });
+
+ this.recordArrayManager.registerFilteredRecordArray(array, type);
+
+ typeMap.findAllCache = array;
+ return array;
+ },
+
+ /**
+ Takes a type and filter function, and returns a live RecordArray that
+ remains up to date as new records are loaded into the store or created
+ locally.
+
+ The callback function takes a materialized record, and returns true
+ if the record should be included in the filter and false if it should
+ not.
+
+ The filter function is called once on all records for the type when
+ it is created, and then once on each newly loaded or created record.
+
+ If any of a record's properties change, or if it changes state, the
+ filter function will be invoked again to determine whether it should
+ still be in the array.
+
+ Note that the existence of a filter on a type will trigger immediate
+ materialization of all loaded data for a given type, so you might
+ not want to use filters for a type if you are loading many records
+ into the store, many of which are not active at any given time.
+
+ In this scenario, you might want to consider filtering the raw
+ data before loading it into the store.
+
+ @method filter
+ @param {Class} type
+ @param {Function} filter
+ @return {DS.FilteredRecordArray}
+ */
+ filter: function(type, query, filter) {
+ // allow an optional server query
+ if (arguments.length === 3) {
+ this.findQuery(type, query);
+ } else if (arguments.length === 2) {
+ filter = query;
+ }
+
+ var array = DS.FilteredRecordArray.create({
+ type: type,
+ content: Ember.A([]),
+ store: this,
+ manager: this.recordArrayManager,
+ filterFunction: filter
+ });
+
+ this.recordArrayManager.registerFilteredRecordArray(array, type, filter);
+
+ return array;
+ },
+
+ /**
+ This method returns if a certain record is already loaded
+ in the store. Use this function to know beforehand if a find()
+ will result in a request or that it will be a cache hit.
+
+ @param {Class} type
+ @param {string} id
+ @return {boolean}
+ */
+ recordIsLoaded: function(type, id) {
+ if (!this.hasReferenceForId(type, id)) { return false; }
+ return typeof this.referenceForId(type, id).data === 'object';
+ },
+
+ // ............
+ // . UPDATING .
+ // ............
+
+ /**
+ @private
+
+ If the adapter updates attributes or acknowledges creation
+ or deletion, the record will notify the store to update its
+ membership in any filters.
+
+ To avoid thrashing, this method is invoked only once per
+ run loop per record.
+
+ @param {Class} type
+ @param {Number|String} clientId
+ @param {DS.Model} record
+ */
+ dataWasUpdated: function(type, reference, record) {
+ // Because data updates are invoked at the end of the run loop,
+ // it is possible that a record might be deleted after its data
+ // has been modified and this method was scheduled to be called.
+ //
+ // If that's the case, the record would have already been removed
+ // from all record arrays; calling updateRecordArrays would just
+ // add it back. If the record is deleted, just bail. It shouldn't
+ // give us any more trouble after this.
+
+ if (get(record, 'isDeleted')) { return; }
+
+ if (typeof reference.data === "object") {
+ this.recordArrayManager.referenceDidChange(reference);
+ }
+ },
+
+ // ..............
+ // . PERSISTING .
+ // ..............
+
+ /**
+ This method delegates saving to the store's implicit
+ transaction.
+
+ Calling this method is essentially a request to persist
+ any changes to records that were not explicitly added to
+ a transaction.
+ */
+ save: function() {
+ once(this, 'commitDefaultTransaction');
+ },
+ commit: Ember.aliasMethod('save'),
+
+ commitDefaultTransaction: function() {
+ get(this, 'defaultTransaction').commit();
+ },
+
+ scheduleSave: function(record) {
+ get(this, 'currentTransaction').add(record);
+ once(this, 'flushSavedRecords');
+ },
+
+ flushSavedRecords: function() {
+ get(this, 'currentTransaction').commit();
+ set(this, 'currentTransaction', this.transaction());
+ },
+
+ /**
+ Adapters should call this method if they would like to acknowledge
+ that all changes related to a record (other than relationship
+ changes) have persisted.
+
+ Because relationship changes affect multiple records, the adapter
+ is responsible for acknowledging the change to the relationship
+ directly (using `store.didUpdateRelationship`) when all aspects
+ of the relationship change have persisted.
+
+ It can be called for created, deleted or updated records.
+
+ If the adapter supplies new data, that data will become the new
+ canonical data for the record. That will result in blowing away
+ all local changes and rematerializing the record with the new
+ data (the "sledgehammer" approach).
+
+ Alternatively, if the adapter does not supply new data, the record
+ will collapse all local changes into its saved data. Subsequent
+ rollbacks of the record will roll back to this point.
+
+ If an adapter is acknowledging receipt of a newly created record
+ that did not generate an id in the client, it *must* either
+ provide data or explicitly invoke `store.didReceiveId` with
+ the server-provided id.
+
+ Note that an adapter may not supply new data when acknowledging
+ a deleted record.
+
+ @see DS.Store#didUpdateRelationship
+
+ @param {DS.Model} record the in-flight record
+ @param {Object} data optional data (see above)
+ */
+ didSaveRecord: function(record, data) {
+ if (data) {
+ this.updateId(record, data);
+ this.updateRecordData(record, data);
+ } else {
+ this.didUpdateAttributes(record);
+ }
+
+ record.adapterDidCommit();
+ },
+
+ /**
+ For convenience, if an adapter is performing a bulk commit, it can also
+ acknowledge all of the records at once.
+
+ If the adapter supplies an array of data, they must be in the same order as
+ the array of records passed in as the first parameter.
+
+ @param {#forEach} list a list of records whose changes the
+ adapter is acknowledging. You can pass any object that
+ has an ES5-like `forEach` method, including the
+ `OrderedSet` objects passed into the adapter at commit
+ time.
+ @param {Array[Object]} dataList an Array of data. This
+ parameter must be an integer-indexed Array-like.
+ */
+ didSaveRecords: function(list, dataList) {
+ var i = 0;
+ list.forEach(function(record) {
+ this.didSaveRecord(record, dataList && dataList[i++]);
+ }, this);
+ },
+
+ /**
+ This method allows the adapter to specify that a record
+ could not be saved because it had backend-supplied validation
+ errors.
+
+ The errors object must have keys that correspond to the
+ attribute names. Once each of the specified attributes have
+ changed, the record will automatically move out of the
+ invalid state and be ready to commit again.
+
+ TODO: We should probably automate the process of converting
+ server names to attribute names using the existing serializer
+ infrastructure.
+
+ @param {DS.Model} record
+ @param {Object} errors
+ */
+ recordWasInvalid: function(record, errors) {
+ record.adapterDidInvalidate(errors);
+ },
+
+ /**
+ This method allows the adapter to specify that a record
+ could not be saved because the server returned an unhandled
+ error.
+
+ @param {DS.Model} record
+ */
+ recordWasError: function(record) {
+ record.adapterDidError();
+ },
+
+ /**
+ This is a lower-level API than `didSaveRecord` that allows an
+ adapter to acknowledge the persistence of a single attribute.
+
+ This is useful if an adapter needs to make multiple asynchronous
+ calls to fully persist a record. The record will keep track of
+ which attributes and relationships are still outstanding and
+ automatically move into the `saved` state once the adapter has
+ acknowledged everything.
+
+ If a value is provided, it clobbers the locally specified value.
+ Otherwise, the local value becomes the record's last known
+ saved value (which is used when rolling back a record).
+
+ Note that the specified attributeName is the normalized name
+ specified in the definition of the `DS.Model`, not a key in
+ the server-provided data.
+
+ Also note that the adapter is responsible for performing any
+ transformations on the value using the serializer API.
+
+ @param {DS.Model} record
+ @param {String} attributeName
+ @param {Object} value
+ */
+ didUpdateAttribute: function(record, attributeName, value) {
+ record.adapterDidUpdateAttribute(attributeName, value);
+ },
+
+ /**
+ This method allows an adapter to acknowledge persistence
+ of all attributes of a record but not relationships or
+ other factors.
+
+ It loops through the record's defined attributes and
+ notifies the record that they are all acknowledged.
+
+ This method does not take optional values, because
+ the adapter is unlikely to have a hash of normalized
+ keys and transformed values, and instead of building
+ one up, it should just call `didUpdateAttribute` as
+ needed.
+
+ This method is intended as a middle-ground between
+ `didSaveRecord`, which acknowledges all changes to
+ a record, and `didUpdateAttribute`, which allows an
+ adapter fine-grained control over updates.
+
+ @param {DS.Model} record
+ */
+ didUpdateAttributes: function(record) {
+ record.eachAttribute(function(attributeName) {
+ this.didUpdateAttribute(record, attributeName);
+ }, this);
+ },
+
+ /**
+ This allows an adapter to acknowledge that it has saved all
+ necessary aspects of a relationship change.
+
+ This is separated from acknowledging the record itself
+ (via `didSaveRecord`) because a relationship change can
+ involve as many as three separate records. Records should
+ only move out of the in-flight state once the server has
+ acknowledged all of their relationships, and this differs
+ based upon the adapter's semantics.
+
+ There are three basic scenarios by which an adapter can
+ save a relationship.
+
+ ### Foreign Key
+
+ An adapter can save all relationship changes by updating
+ a foreign key on the child record. If it does this, it
+ should acknowledge the changes when the child record is
+ saved.
+
+ record.eachRelationship(function(name, meta) {
+ if (meta.kind === 'belongsTo') {
+ store.didUpdateRelationship(record, name);
+ }
+ });
+
+ store.didSaveRecord(record, data);
+
+ ### Embedded in Parent
+
+ An adapter can save one-to-many relationships by embedding
+ IDs (or records) in the parent object. In this case, the
+ relationship is not considered acknowledged until both the
+ old parent and new parent have acknowledged the change.
+
+ In this case, the adapter should keep track of the old
+ parent and new parent, and acknowledge the relationship
+ change once both have acknowledged. If one of the two
+ sides does not exist (e.g. the new parent does not exist
+ because of nulling out the belongs-to relationship),
+ the adapter should acknowledge the relationship once
+ the other side has acknowledged.
+
+ ### Separate Entity
+
+ An adapter can save relationships as separate entities
+ on the server. In this case, they should acknowledge
+ the relationship as saved once the server has
+ acknowledged the entity.
+
+ @see DS.Store#didSaveRecord
+
+ @param {DS.Model} record
+ @param {DS.Model} relationshipName
+ */
+ didUpdateRelationship: function(record, relationshipName) {
+ var clientId = get(record, '_reference').clientId;
+
+ var relationship = this.relationshipChangeFor(clientId, relationshipName);
+ //TODO(Igor)
+ if (relationship) { relationship.adapterDidUpdate(); }
+ },
+
+ /**
+ This allows an adapter to acknowledge all relationship changes
+ for a given record.
+
+ Like `didUpdateAttributes`, this is intended as a middle ground
+ between `didSaveRecord` and fine-grained control via the
+ `didUpdateRelationship` API.
+ */
+ didUpdateRelationships: function(record) {
+ var changes = this.relationshipChangesFor(get(record, '_reference'));
+
+ for (var name in changes) {
+ if (!changes.hasOwnProperty(name)) { continue; }
+ changes[name].adapterDidUpdate();
+ }
+ },
+
+ /**
+ When acknowledging the creation of a locally created record,
+ adapters must supply an id (if they did not implement
+ `generateIdForRecord` to generate an id locally).
+
+ If an adapter does not use `didSaveRecord` and supply a hash
+ (for example, if it needs to make multiple HTTP requests to
+ create and then update the record), it will need to invoke
+ `didReceiveId` with the backend-supplied id.
+
+ When not using `didSaveRecord`, an adapter will need to
+ invoke:
+
+ * didReceiveId (unless the id was generated locally)
+ * didCreateRecord
+ * didUpdateAttribute(s)
+ * didUpdateRelationship(s)
+
+ @param {DS.Model} record
+ @param {Number|String} id
+ */
+ didReceiveId: function(record, id) {
+ var typeMap = this.typeMapFor(record.constructor),
+ clientId = get(record, 'clientId'),
+ oldId = get(record, 'id');
+
+ Ember.assert("An adapter cannot assign a new id to a record that already has an id. " + record + " had id: " + oldId + " and you tried to update it with " + id + ". This likely happened because your server returned data in response to a find or update that had a different id than the one you sent.", oldId === undefined || id === oldId);
+
+ typeMap.idToCid[id] = clientId;
+ this.clientIdToId[clientId] = id;
+ },
+
+ /**
+ @private
+
+ This method re-indexes the data by its clientId in the store
+ and then notifies the record that it should rematerialize
+ itself.
+
+ @param {DS.Model} record
+ @param {Object} data
+ */
+ updateRecordData: function(record, data) {
+ get(record, '_reference').data = data;
+ record.didChangeData();
+ },
+
+ /**
+ @private
+
+ If an adapter invokes `didSaveRecord` with data, this method
+ extracts the id from the supplied data (using the adapter's
+ `extractId()` method) and indexes the clientId with that id.
+
+ @param {DS.Model} record
+ @param {Object} data
+ */
+ updateId: function(record, data) {
+ var type = record.constructor,
+ typeMap = this.typeMapFor(type),
+ reference = get(record, '_reference'),
+ oldId = get(record, 'id'),
+ id = this.preprocessData(type, data);
+
+ Ember.assert("An adapter cannot assign a new id to a record that already has an id. " + record + " had id: " + oldId + " and you tried to update it with " + id + ". This likely happened because your server returned data in response to a find or update that had a different id than the one you sent.", oldId === null || id === oldId);
+
+ typeMap.idToReference[id] = reference;
+ reference.id = id;
+ },
+
+ /**
+ @private
+
+ This method receives opaque data provided by the adapter and
+ preprocesses it, returning an ID.
+
+ The actual preprocessing takes place in the adapter. If you would
+ like to change the default behavior, you should override the
+ appropriate hooks in `DS.Serializer`.
+
+ @see {DS.Serializer}
+ @return {String} id the id represented by the data
+ */
+ preprocessData: function(type, data) {
+ return this.adapterForType(type).extractId(type, data);
+ },
+
+ /** @private
+ Returns a map of IDs to client IDs for a given type.
+ */
+ typeMapFor: function(type) {
+ var typeMaps = get(this, 'typeMaps'),
+ guid = Ember.guidFor(type),
+ typeMap;
+
+ typeMap = typeMaps[guid];
+
+ if (typeMap) { return typeMap; }
+
+ typeMap = {
+ idToReference: {},
+ references: [],
+ metadata: {}
+ };
+
+ typeMaps[guid] = typeMap;
+
+ return typeMap;
+ },
+
+ // ................
+ // . LOADING DATA .
+ // ................
+
+ /**
+ Load new data into the store for a given id and type combination.
+ If data for that record had been loaded previously, the new information
+ overwrites the old.
+
+ If the record you are loading data for has outstanding changes that have not
+ yet been saved, an exception will be thrown.
+
+ @param {DS.Model} type
+ @param {String|Number} id
+ @param {Object} data the data to load
+ */
+ load: function(type, data, prematerialized) {
+ var id;
+
+ if (typeof data === 'number' || typeof data === 'string') {
+ id = data;
+ data = prematerialized;
+ prematerialized = null;
+ }
+
+ if (prematerialized && prematerialized.id) {
+ id = prematerialized.id;
+ } else if (id === undefined) {
+ id = this.preprocessData(type, data);
+ }
+
+ id = coerceId(id);
+
+ var reference = this.referenceForId(type, id);
+
+ if (reference.record) {
+ once(reference.record, 'loadedData');
+ }
+
+ reference.data = data;
+ reference.prematerialized = prematerialized;
+
+ this.recordArrayManager.referenceDidChange(reference);
+
+ return reference;
+ },
+
+ loadMany: function(type, ids, dataList) {
+ if (dataList === undefined) {
+ dataList = ids;
+ ids = map(dataList, function(data) {
+ return this.preprocessData(type, data);
+ }, this);
+ }
+
+ return map(ids, function(id, i) {
+ return this.load(type, id, dataList[i]);
+ }, this);
+ },
+
+ loadHasMany: function(record, key, ids) {
+ //It looks sad to have to do the conversion in the store
+ var type = record.get(key + '.type'),
+ tuples = map(ids, function(id) {
+ return {id: id, type: type};
+ });
+ record.materializeHasMany(key, tuples);
+
+ // Update any existing many arrays that use the previous IDs,
+ // if necessary.
+ record.hasManyDidChange(key);
+
+ var relationship = record.cacheFor(key);
+
+ // TODO (tomdale) this assumes that loadHasMany *always* means
+ // that the records for the provided IDs are loaded.
+ if (relationship) {
+ set(relationship, 'isLoaded', true);
+ relationship.trigger('didLoad');
+ }
+ },
+
+ /** @private
+
+ Creates a new reference for a given type & ID pair. Metadata about the
+ record can be stored in the reference without having to create a full-blown
+ DS.Model instance.
+
+ @param {DS.Model} type
+ @param {String|Number} id
+ @returns {Reference}
+ */
+ createReference: function(type, id) {
+ var typeMap = this.typeMapFor(type),
+ idToReference = typeMap.idToReference;
+
+ Ember.assert('The id ' + id + ' has already been used with another record of type ' + type.toString() + '.', !id || !idToReference[id]);
+
+ var reference = {
+ id: id,
+ clientId: this.clientIdCounter++,
+ type: type
+ };
+
+ // if we're creating an item, this process will be done
+ // later, once the object has been persisted.
+ if (id) {
+ idToReference[id] = reference;
+ }
+
+ typeMap.references.push(reference);
+
+ return reference;
+ },
+
+ // ..........................
+ // . RECORD MATERIALIZATION .
+ // ..........................
+
+ materializeRecord: function(reference) {
+ var record = reference.type._create({
+ id: reference.id,
+ store: this,
+ _reference: reference
+ });
+
+ reference.record = record;
+
+ get(this, 'defaultTransaction').adoptRecord(record);
+
+ record.loadingData();
+
+ if (typeof reference.data === 'object') {
+ record.loadedData();
+ }
+
+ return record;
+ },
+
+ dematerializeRecord: function(record) {
+ var reference = get(record, '_reference'),
+ type = reference.type,
+ id = reference.id,
+ typeMap = this.typeMapFor(type);
+
+ record.updateRecordArrays();
+
+ if (id) { delete typeMap.idToReference[id]; }
+
+ var loc = typeMap.references.indexOf(reference);
+ typeMap.references.splice(loc, 1);
+ },
+
+ willDestroy: function() {
+ if (get(DS, 'defaultStore') === this) {
+ set(DS, 'defaultStore', null);
+ }
+ },
+
+ // ........................
+ // . RELATIONSHIP CHANGES .
+ // ........................
+
+ addRelationshipChangeFor: function(clientReference, childKey, parentReference, parentKey, change) {
+ var clientId = clientReference.clientId,
+ parentClientId = parentReference ? parentReference.clientId : parentReference;
+ var key = childKey + parentKey;
+ var changes = this.relationshipChanges;
+ if (!(clientId in changes)) {
+ changes[clientId] = {};
+ }
+ if (!(parentClientId in changes[clientId])) {
+ changes[clientId][parentClientId] = {};
+ }
+ if (!(key in changes[clientId][parentClientId])) {
+ changes[clientId][parentClientId][key] = {};
+ }
+ changes[clientId][parentClientId][key][change.changeType] = change;
+ },
+
+ removeRelationshipChangeFor: function(clientReference, childKey, parentReference, parentKey, type) {
+ var clientId = clientReference.clientId,
+ parentClientId = parentReference ? parentReference.clientId : parentReference;
+ var changes = this.relationshipChanges;
+ var key = childKey + parentKey;
+ if (!(clientId in changes) || !(parentClientId in changes[clientId]) || !(key in changes[clientId][parentClientId])){
+ return;
+ }
+ delete changes[clientId][parentClientId][key][type];
+ },
+
+ relationshipChangeFor: function(clientReference, childKey, parentReference, parentKey, type) {
+ var clientId = clientReference.clientId,
+ parentClientId = parentReference ? parentReference.clientId : parentReference;
+ var changes = this.relationshipChanges;
+ var key = childKey + parentKey;
+ if (!(clientId in changes) || !(parentClientId in changes[clientId])){
+ return;
+ }
+ if(type){
+ return changes[clientId][parentClientId][key][type];
+ }
+ else{
+ //TODO(Igor) what if both present
+ return changes[clientId][parentClientId][key]["add"] || changes[clientId][parentClientId][key]["remove"];
+ }
+ },
+
+ relationshipChangePairsFor: function(reference){
+ var toReturn = [];
+
+ if( !reference ) { return toReturn; }
+
+ //TODO(Igor) What about the other side
+ var changesObject = this.relationshipChanges[reference.clientId];
+ for (var objKey in changesObject){
+ if(changesObject.hasOwnProperty(objKey)){
+ for (var changeKey in changesObject[objKey]){
+ if(changesObject[objKey].hasOwnProperty(changeKey)){
+ toReturn.push(changesObject[objKey][changeKey]);
+ }
+ }
+ }
+ }
+ return toReturn;
+ },
+
+ relationshipChangesFor: function(reference) {
+ var toReturn = [];
+
+ if( !reference ) { return toReturn; }
+
+ var relationshipPairs = this.relationshipChangePairsFor(reference);
+ forEach(relationshipPairs, function(pair){
+ var addedChange = pair["add"];
+ var removedChange = pair["remove"];
+ if(addedChange){
+ toReturn.push(addedChange);
+ }
+ if(removedChange){
+ toReturn.push(removedChange);
+ }
+ });
+ return toReturn;
+ },
+ // ......................
+ // . PER-TYPE ADAPTERS
+ // ......................
+
+ adapterForType: function(type) {
+ this._adaptersMap = this.createInstanceMapFor('adapters');
+
+ var adapter = this._adaptersMap.get(type);
+ if (adapter) { return adapter; }
+
+ return this.get('_adapter');
+ },
+
+ // ..............................
+ // . RECORD CHANGE NOTIFICATION .
+ // ..............................
+
+ recordAttributeDidChange: function(reference, attributeName, newValue, oldValue) {
+ var record = reference.record,
+ dirtySet = new Ember.OrderedSet(),
+ adapter = this.adapterForType(record.constructor);
+
+ if (adapter.dirtyRecordsForAttributeChange) {
+ adapter.dirtyRecordsForAttributeChange(dirtySet, record, attributeName, newValue, oldValue);
+ }
+
+ dirtySet.forEach(function(record) {
+ record.adapterDidDirty();
+ });
+ },
+
+ recordBelongsToDidChange: function(dirtySet, child, relationship) {
+ var adapter = this.adapterForType(child.constructor);
+
+ if (adapter.dirtyRecordsForBelongsToChange) {
+ adapter.dirtyRecordsForBelongsToChange(dirtySet, child, relationship);
+ }
+
+ // adapterDidDirty is called by the RelationshipChange that created
+ // the dirtySet.
+ },
+
+ recordHasManyDidChange: function(dirtySet, parent, relationship) {
+ var adapter = this.adapterForType(parent.constructor);
+
+ if (adapter.dirtyRecordsForHasManyChange) {
+ adapter.dirtyRecordsForHasManyChange(dirtySet, parent, relationship);
+ }
+
+ // adapterDidDirty is called by the RelationshipChange that created
+ // the dirtySet.
+ }
+});
+
+DS.Store.reopenClass({
+ registerAdapter: DS._Mappable.generateMapFunctionFor('adapters', function(type, adapter, map) {
+ map.set(type, adapter);
+ }),
+
+ transformMapKey: function(key) {
+ if (typeof key === 'string') {
+ var transformedKey;
+ transformedKey = get(Ember.lookup, key);
+ Ember.assert("Could not find model at path " + key, transformedKey);
+ return transformedKey;
+ } else {
+ return key;
+ }
+ },
+
+ transformMapValue: function(key, value) {
+ if (Ember.Object.detect(value)) {
+ return value.create();
+ }
+
+ return value;
+ }
+});
+
+})();
+
+
+
+(function() {
+/**
+ @module data
+ @submodule data-model
+*/
+
+var get = Ember.get, set = Ember.set,
+ once = Ember.run.once, arrayMap = Ember.ArrayPolyfills.map;
+
+/**
+ This file encapsulates the various states that a record can transition
+ through during its lifecycle.
+
+ ### State Manager
+
+ A record's state manager 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
+ `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 `updated.inFlight` state. (These state paths will be explained
+ in more detail below.)
+
+ Events are sent by the record or its store to the record's state manager.
+ How the state manager 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. For example, a record can be in the
+ `deleted.start` state, then transition into the `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 its manager's
+ current state path:
+
+ record.get('stateManager.currentPath');
+ //=> "created.uncommitted"
+
+ The `DS.Model` states are themselves stateless. What we mean is that,
+ though each instance of a record also has a unique instance of a
+ `DS.StateManager`, 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 state
+ manager points to this global, immutable shared instance. How does a
+ state know which record it should be acting on? We pass a reference to
+ the current state manager as the first parameter to every method invoked
+ on a state.
+
+ The state manager 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. If you need access to the record being acted on, you can
+ retrieve the state manager's `record` property. For example, if you had
+ an event handler `myEvent`:
+
+ myEvent: function(manager) {
+ var record = manager.get('record');
+ record.doSomething();
+ }
+
+ For more information about state managers in general, see the Ember.js
+ documentation on `Ember.StateManager`.
+
+ ### Events, Flags, and Transitions
+
+ A state may implement zero or more events, flags, or transitions.
+
+ #### Events
+
+ Events are named functions that are invoked when sent to a record. The
+ state manager 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:
+
+ aState: DS.State.create({
+ myEvent: function(manager, param) {
+ console.log("Received myEvent with "+param);
+ }
+ })
+
+ To trigger this event:
+
+ 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 state manager'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
+ * start <-- currentState
+ * inFlight
+ * updated
+ * inFlight
+
+ If we are currently in the `start` 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:
+
+ var statePath = record.get('stateManager.currentPath');
+ if (statePath === 'created.inFlight') {
+ doSomething();
+ }
+
+ You can say:
+
+ 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.
+
+ #### Transitions
+
+ Transitions are like event handlers but are called automatically upon
+ entering or exiting a state. To implement a transition, just call a method
+ either `enter` or `exit`:
+
+ myState: DS.State.create({
+ // Gets called automatically when entering
+ // this state.
+ enter: function(manager) {
+ console.log("Entered myState");
+ }
+ })
+
+ Note that enter and exit events are called once per transition. If the
+ current state changes, but changes to another child state of the parent,
+ the transition event on the parent will not be triggered.
+
+ @class States
+ @namespace DS
+ @extends Ember.State
+*/
+
+var stateProperty = Ember.computed(function(key) {
+ var parent = get(this, 'parentState');
+ if (parent) {
+ return get(parent, key);
+ }
+}).property();
+
+var hasDefinedProperties = function(object) {
+ for (var name in object) {
+ if (object.hasOwnProperty(name) && object[name]) { return true; }
+ }
+
+ return false;
+};
+
+var didChangeData = function(manager) {
+ var record = get(manager, 'record');
+ record.materializeData();
+};
+
+var willSetProperty = function(manager, context) {
+ context.oldValue = get(get(manager, 'record'), context.name);
+
+ var change = DS.AttributeChange.createChange(context);
+ get(manager, 'record')._changesToSync[context.name] = change;
+};
+
+var didSetProperty = function(manager, context) {
+ var change = get(manager, 'record')._changesToSync[context.name];
+ change.value = get(get(manager, 'record'), context.name);
+ change.sync();
+};
+
+DS.State = Ember.State.extend({
+ isLoading: stateProperty,
+ isLoaded: stateProperty,
+ isReloading: stateProperty,
+ isDirty: stateProperty,
+ isSaving: stateProperty,
+ isDeleted: stateProperty,
+ isError: stateProperty,
+ isNew: stateProperty,
+ isValid: stateProperty,
+
+ // For states that are substates of a
+ // DirtyState (updated or created), it is
+ // useful to be able to determine which
+ // type of dirty state it is.
+ dirtyType: stateProperty
+});
+
+// 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's transaction 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: No client-side validations have failed and 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 = DS.State.extend({
+ 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: DS.State.extend({
+
+ // EVENTS
+ willSetProperty: willSetProperty,
+ didSetProperty: didSetProperty,
+
+ becomeDirty: Ember.K,
+
+ willCommit: function(manager) {
+ manager.transitionTo('inFlight');
+ },
+
+ becameClean: function(manager) {
+ var record = get(manager, 'record');
+
+ record.withTransaction(function(t) {
+ t.remove(record);
+ });
+ manager.transitionTo('loaded.materializing');
+ },
+
+ becameInvalid: function(manager) {
+ manager.transitionTo('invalid');
+ },
+
+ rollback: function(manager) {
+ get(manager, '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: DS.State.extend({
+ // FLAGS
+ isSaving: true,
+
+ // TRANSITIONS
+ enter: function(manager) {
+ var record = get(manager, 'record');
+
+ record.becameInFlight();
+ },
+
+ // EVENTS
+
+ materializingData: function(manager) {
+ set(manager, 'lastDirtyType', get(this, 'dirtyType'));
+ manager.transitionTo('materializing');
+ },
+
+ didCommit: function(manager) {
+ var dirtyType = get(this, 'dirtyType'),
+ record = get(manager, 'record');
+
+ record.withTransaction(function(t) {
+ t.remove(record);
+ });
+
+ manager.transitionTo('saved');
+ manager.send('invokeLifecycleCallbacks', dirtyType);
+ },
+
+ didChangeData: didChangeData,
+
+ becameInvalid: function(manager, errors) {
+ var record = get(manager, 'record');
+
+ set(record, 'errors', errors);
+
+ manager.transitionTo('invalid');
+ manager.send('invokeLifecycleCallbacks');
+ },
+
+ becameError: function(manager) {
+ manager.transitionTo('error');
+ manager.send('invokeLifecycleCallbacks');
+ }
+ }),
+
+ // A record is in the `invalid` state when its client-side
+ // invalidations have failed, or if the adapter has indicated
+ // the the record failed server-side invalidations.
+ invalid: DS.State.extend({
+ // FLAGS
+ isValid: false,
+
+ exit: function(manager) {
+ var record = get(manager, 'record');
+
+ record.withTransaction(function (t) {
+ t.remove(record);
+ });
+ },
+
+ // EVENTS
+ deleteRecord: function(manager) {
+ manager.transitionTo('deleted');
+ get(manager, 'record').clearRelationships();
+ },
+
+ willSetProperty: willSetProperty,
+
+ didSetProperty: function(manager, context) {
+ var record = get(manager, 'record'),
+ errors = get(record, 'errors'),
+ key = context.name;
+
+ set(errors, key, null);
+
+ if (!hasDefinedProperties(errors)) {
+ manager.send('becameValid');
+ }
+
+ didSetProperty(manager, context);
+ },
+
+ becomeDirty: Ember.K,
+
+ rollback: function(manager) {
+ manager.send('becameValid');
+ manager.send('rollback');
+ },
+
+ becameValid: function(manager) {
+ manager.transitionTo('uncommitted');
+ },
+
+ invokeLifecycleCallbacks: function(manager) {
+ var record = get(manager, 'record');
+ record.trigger('becameInvalid', record);
+ }
+ })
+});
+
+// The created and updated states are created outside the state
+// chart so we can reopen their substates and add mixins as
+// necessary.
+
+var createdState = DirtyState.create({
+ dirtyType: 'created',
+
+ // FLAGS
+ isNew: true
+});
+
+var updatedState = DirtyState.create({
+ dirtyType: 'updated'
+});
+
+createdState.states.uncommitted.reopen({
+ deleteRecord: function(manager) {
+ var record = get(manager, 'record');
+
+ record.clearRelationships();
+ manager.transitionTo('deleted.saved');
+ }
+});
+
+createdState.states.uncommitted.reopen({
+ rollback: function(manager) {
+ this._super(manager);
+ manager.transitionTo('deleted.saved');
+ }
+});
+
+updatedState.states.uncommitted.reopen({
+ deleteRecord: function(manager) {
+ var record = get(manager, 'record');
+
+ manager.transitionTo('deleted');
+ record.clearRelationships();
+ }
+});
+
+var states = {
+ rootState: Ember.State.create({
+ // FLAGS
+ isLoading: false,
+ isLoaded: false,
+ isReloading: false,
+ isDirty: false,
+ isSaving: false,
+ isDeleted: false,
+ isError: false,
+ isNew: false,
+ isValid: true,
+
+ // 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: DS.State.create({
+ // EVENTS
+ loadingData: function(manager) {
+ manager.transitionTo('loading');
+ },
+
+ loadedData: function(manager) {
+ manager.transitionTo('loaded.created');
+ }
+ }),
+
+ // A record enters this state when the store askes
+ // 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: DS.State.create({
+ // FLAGS
+ isLoading: true,
+
+ // EVENTS
+ loadedData: didChangeData,
+
+ materializingData: function(manager) {
+ manager.transitionTo('loaded.materializing.firstTime');
+ },
+
+ becameError: function(manager) {
+ manager.transitionTo('error');
+ manager.send('invokeLifecycleCallbacks');
+ }
+ }),
+
+ // 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: DS.State.create({
+ initialState: 'saved',
+
+ // FLAGS
+ isLoaded: true,
+
+ // SUBSTATES
+
+ materializing: DS.State.create({
+ // EVENTS
+ willSetProperty: Ember.K,
+ didSetProperty: Ember.K,
+
+ didChangeData: didChangeData,
+
+ finishedMaterializing: function(manager) {
+ manager.transitionTo('loaded.saved');
+ },
+
+ // SUBSTATES
+ firstTime: DS.State.create({
+ // FLAGS
+ isLoaded: false,
+
+ exit: function(manager) {
+ var record = get(manager, 'record');
+
+ once(function() {
+ record.trigger('didLoad');
+ });
+ }
+ })
+ }),
+
+ reloading: DS.State.create({
+ // FLAGS
+ isReloading: true,
+
+ // TRANSITIONS
+ enter: function(manager) {
+ var record = get(manager, 'record'),
+ store = get(record, 'store');
+
+ store.reloadRecord(record);
+ },
+
+ exit: function(manager) {
+ var record = get(manager, 'record');
+
+ once(record, 'trigger', 'didReload');
+ },
+
+ // EVENTS
+ loadedData: didChangeData,
+
+ materializingData: function(manager) {
+ manager.transitionTo('loaded.materializing');
+ }
+ }),
+
+ // If there are no local changes to a record, it remains
+ // in the `saved` state.
+ saved: DS.State.create({
+ // EVENTS
+ willSetProperty: willSetProperty,
+ didSetProperty: didSetProperty,
+
+ didChangeData: didChangeData,
+ loadedData: didChangeData,
+
+ reloadRecord: function(manager) {
+ manager.transitionTo('loaded.reloading');
+ },
+
+ materializingData: function(manager) {
+ manager.transitionTo('loaded.materializing');
+ },
+
+ becomeDirty: function(manager) {
+ manager.transitionTo('updated');
+ },
+
+ deleteRecord: function(manager) {
+ manager.transitionTo('deleted');
+ get(manager, 'record').clearRelationships();
+ },
+
+ unloadRecord: function(manager) {
+ var record = get(manager, 'record');
+
+ // clear relationships before moving to deleted state
+ // otherwise it fails
+ record.clearRelationships();
+ manager.transitionTo('deleted.saved');
+ },
+
+ didCommit: function(manager) {
+ var record = get(manager, 'record');
+
+ record.withTransaction(function(t) {
+ t.remove(record);
+ });
+
+ manager.send('invokeLifecycleCallbacks', get(manager, 'lastDirtyType'));
+ },
+
+ invokeLifecycleCallbacks: function(manager, dirtyType) {
+ var record = get(manager, 'record');
+ if (dirtyType === 'created') {
+ record.trigger('didCreate', record);
+ } else {
+ record.trigger('didUpdate', record);
+ }
+
+ record.trigger('didCommit', record);
+ }
+ }),
+
+ // 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: DS.State.create({
+ initialState: 'uncommitted',
+ dirtyType: 'deleted',
+
+ // FLAGS
+ isDeleted: true,
+ isLoaded: true,
+ isDirty: true,
+
+ // TRANSITIONS
+ setup: function(manager) {
+ var record = get(manager, 'record'),
+ store = get(record, 'store');
+
+ store.recordArrayManager.remove(record);
+ },
+
+ // SUBSTATES
+
+ // When a record is deleted, it enters the `start`
+ // state. It will exit this state when the record's
+ // transaction starts to commit.
+ uncommitted: DS.State.create({
+
+ // EVENTS
+ willCommit: function(manager) {
+ manager.transitionTo('inFlight');
+ },
+
+ rollback: function(manager) {
+ get(manager, 'record').rollback();
+ },
+
+ becomeDirty: Ember.K,
+
+ becameClean: function(manager) {
+ var record = get(manager, 'record');
+
+ record.withTransaction(function(t) {
+ t.remove(record);
+ });
+ manager.transitionTo('loaded.materializing');
+ }
+ }),
+
+ // After a record's transaction is committing, but
+ // before the adapter indicates that the deletion
+ // has saved to the server, a record is in the
+ // `inFlight` substate of `deleted`.
+ inFlight: DS.State.create({
+ // FLAGS
+ isSaving: true,
+
+ // TRANSITIONS
+ enter: function(manager) {
+ var record = get(manager, 'record');
+
+ record.becameInFlight();
+ },
+
+ // EVENTS
+ didCommit: function(manager) {
+ var record = get(manager, 'record');
+
+ record.withTransaction(function(t) {
+ t.remove(record);
+ });
+
+ manager.transitionTo('saved');
+
+ manager.send('invokeLifecycleCallbacks');
+ }
+ }),
+
+ // Once the adapter indicates that the deletion has
+ // been saved, the record enters the `saved` substate
+ // of `deleted`.
+ saved: DS.State.create({
+ // FLAGS
+ isDirty: false,
+
+ setup: function(manager) {
+ var record = get(manager, 'record'),
+ store = get(record, 'store');
+
+ store.dematerializeRecord(record);
+ },
+
+ invokeLifecycleCallbacks: function(manager) {
+ var record = get(manager, 'record');
+ record.trigger('didDelete', record);
+ record.trigger('didCommit', record);
+ }
+ })
+ }),
+
+ // If the adapter indicates that there was an unknown
+ // error saving a record, the record enters the `error`
+ // state.
+ error: DS.State.create({
+ isError: true,
+
+ // EVENTS
+
+ invokeLifecycleCallbacks: function(manager) {
+ var record = get(manager, 'record');
+ record.trigger('becameError', record);
+ }
+ })
+ })
+};
+
+DS.StateManager = Ember.StateManager.extend({
+ record: null,
+ initialState: 'rootState',
+ states: states,
+ unhandledEvent: function(manager, originalEvent) {
+ var record = manager.get('record'),
+ contexts = [].slice.call(arguments, 2),
+ errorMessage;
+ errorMessage = "Attempted to handle event `" + originalEvent + "` ";
+ errorMessage += "on " + record.toString() + " while in state ";
+ errorMessage += get(manager, 'currentState.path') + ". Called with ";
+ errorMessage += arrayMap.call(contexts, function(context){
+ return Ember.inspect(context);
+ }).join(', ');
+ throw new Ember.Error(errorMessage);
+ }
+});
+
+})();
+
+
+
+(function() {
+var LoadPromise = DS.LoadPromise; // system/mixins/load_promise
+
+var get = Ember.get, set = Ember.set, map = Ember.EnumerableUtils.map;
+
+var retrieveFromCurrentState = Ember.computed(function(key, value) {
+ return get(get(this, 'stateManager.currentState'), key);
+}).property('stateManager.currentState').readOnly();
+
+/**
+
+ The model class that all Ember Data records descend from.
+
+ @module data
+ @submodule data-model
+ @main data-model
+
+ @class Model
+ @namespace DS
+ @extends Ember.Object
+ @constructor
+*/
+
+DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, {
+ isLoading: retrieveFromCurrentState,
+ isLoaded: retrieveFromCurrentState,
+ isReloading: retrieveFromCurrentState,
+ isDirty: retrieveFromCurrentState,
+ isSaving: retrieveFromCurrentState,
+ isDeleted: retrieveFromCurrentState,
+ isError: retrieveFromCurrentState,
+ isNew: retrieveFromCurrentState,
+ isValid: retrieveFromCurrentState,
+ dirtyType: retrieveFromCurrentState,
+
+ clientId: null,
+ id: null,
+ transaction: null,
+ stateManager: null,
+ errors: null,
+
+ /**
+ Create a JSON representation of the record, using the serialization
+ strategy of the store's adapter.
+
+ @method serialize
+ @param {Object} options Available options:
+
+ * `includeId`: `true` if the record's ID should be included in the
+ JSON representation.
+
+ @returns {Object} an object whose values are primitive JSON values only
+ */
+ serialize: function(options) {
+ var store = get(this, 'store');
+ return store.serialize(this, options);
+ },
+
+ /**
+ Use {{#crossLink "DS.JSONSerializer"}}DS.JSONSerializer{{/crossLink}} to
+ get the JSON representation of a record.
+
+ @method toJSON
+ @param {Object} options Available options:
+
+ * `includeId`: `true` if the record's ID should be included in the
+ JSON representation.
+
+ @returns {Object} A JSON representation of the object.
+ */
+ toJSON: function(options) {
+ var serializer = DS.JSONSerializer.create();
+ return serializer.serialize(this, options);
+ },
+
+ /**
+ Fired when the record is loaded from the server.
+
+ @event didLoad
+ */
+ didLoad: Ember.K,
+
+ /**
+ Fired when the record is reloaded from the server.
+
+ @event didReload
+ */
+ didReload: 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,
+
+ data: Ember.computed(function() {
+ if (!this._data) {
+ this.setupData();
+ }
+
+ return this._data;
+ }).property(),
+
+ materializeData: function() {
+ this.send('materializingData');
+
+ get(this, 'store').materializeData(this);
+
+ this.suspendRelationshipObservers(function() {
+ this.notifyPropertyChange('data');
+ });
+ },
+
+ _data: null,
+
+ init: function() {
+ this._super();
+
+ var stateManager = DS.StateManager.create({ record: this });
+ set(this, 'stateManager', stateManager);
+
+ this._setup();
+
+ stateManager.goToState('empty');
+ },
+
+ _setup: function() {
+ this._changesToSync = {};
+ },
+
+ send: function(name, context) {
+ return get(this, 'stateManager').send(name, context);
+ },
+
+ withTransaction: function(fn) {
+ var transaction = get(this, 'transaction');
+ if (transaction) { fn(transaction); }
+ },
+
+ loadingData: function() {
+ this.send('loadingData');
+ },
+
+ loadedData: function() {
+ this.send('loadedData');
+ },
+
+ didChangeData: function() {
+ this.send('didChangeData');
+ },
+
+ deleteRecord: function() {
+ this.send('deleteRecord');
+ },
+
+ unloadRecord: function() {
+ Ember.assert("You can only unload a loaded, non-dirty record.", !get(this, 'isDirty'));
+
+ this.send('unloadRecord');
+ },
+
+ clearRelationships: function() {
+ this.eachRelationship(function(name, relationship) {
+ if (relationship.kind === 'belongsTo') {
+ set(this, name, null);
+ } else if (relationship.kind === 'hasMany') {
+ this.clearHasMany(relationship);
+ }
+ }, this);
+ },
+
+ updateRecordArrays: function() {
+ var store = get(this, 'store');
+ if (store) {
+ store.dataWasUpdated(this.constructor, get(this, '_reference'), this);
+ }
+ },
+
+ /**
+ If the adapter did not return a hash in response to a commit,
+ merge the changed attributes and relationships into the existing
+ saved data.
+ */
+ adapterDidCommit: function() {
+ var attributes = get(this, 'data').attributes;
+
+ get(this.constructor, 'attributes').forEach(function(name, meta) {
+ attributes[name] = get(this, name);
+ }, this);
+
+ this.send('didCommit');
+ this.updateRecordArraysLater();
+ },
+
+ adapterDidDirty: function() {
+ this.send('becomeDirty');
+ this.updateRecordArraysLater();
+ },
+
+ dataDidChange: Ember.observer(function() {
+ this.reloadHasManys();
+ this.send('finishedMaterializing');
+ }, 'data'),
+
+ reloadHasManys: function() {
+ var relationships = get(this.constructor, 'relationshipsByName');
+ this.updateRecordArraysLater();
+ relationships.forEach(function(name, relationship) {
+ if (relationship.kind === 'hasMany') {
+ this.hasManyDidChange(relationship.key);
+ }
+ }, this);
+ },
+
+ hasManyDidChange: function(key) {
+ var cachedValue = this.cacheFor(key);
+
+ if (cachedValue) {
+ var type = get(this.constructor, 'relationshipsByName').get(key).type;
+ var store = get(this, 'store');
+ var ids = this._data.hasMany[key] || [];
+
+ var references = map(ids, function(id) {
+ if (typeof id === 'object') {
+ if( id.clientId ) {
+ // if it was already a reference, return the reference
+ return id;
+ } else {
+ // <id, type> tuple for a polymorphic association.
+ return store.referenceForId(id.type, id.id);
+ }
+ }
+ return store.referenceForId(type, id);
+ });
+
+ set(cachedValue, 'content', Ember.A(references));
+ }
+ },
+
+ updateRecordArraysLater: function() {
+ Ember.run.once(this, this.updateRecordArrays);
+ },
+
+ setupData: function() {
+ this._data = {
+ attributes: {},
+ belongsTo: {},
+ hasMany: {},
+ id: null
+ };
+ },
+
+ materializeId: function(id) {
+ set(this, 'id', id);
+ },
+
+ materializeAttributes: function(attributes) {
+ Ember.assert("Must pass a hash of attributes to materializeAttributes", !!attributes);
+ this._data.attributes = attributes;
+ },
+
+ materializeAttribute: function(name, value) {
+ this._data.attributes[name] = value;
+ },
+
+ materializeHasMany: function(name, tuplesOrReferencesOrOpaque) {
+ var tuplesOrReferencesOrOpaqueType = typeof tuplesOrReferencesOrOpaque;
+
+ if (tuplesOrReferencesOrOpaque && tuplesOrReferencesOrOpaqueType !== 'string' && tuplesOrReferencesOrOpaque.length > 1) {
+ Ember.assert('materializeHasMany expects tuples, references or opaque token, not ' + tuplesOrReferencesOrOpaque[0], tuplesOrReferencesOrOpaque[0].hasOwnProperty('id') && tuplesOrReferencesOrOpaque[0].type);
+ }
+
+ if( tuplesOrReferencesOrOpaqueType === "string" ) {
+ this._data.hasMany[name] = tuplesOrReferencesOrOpaque;
+ } else {
+ var references = tuplesOrReferencesOrOpaque;
+
+ if (tuplesOrReferencesOrOpaque && Ember.isArray(tuplesOrReferencesOrOpaque)) {
+ references = this._convertTuplesToReferences(tuplesOrReferencesOrOpaque);
+ }
+
+ this._data.hasMany[name] = references;
+ }
+ },
+
+ materializeBelongsTo: function(name, tupleOrReference) {
+ if (tupleOrReference) { Ember.assert('materializeBelongsTo expects a tuple or a reference, not a ' + tupleOrReference, !tupleOrReference || (tupleOrReference.hasOwnProperty('id') && tupleOrReference.hasOwnProperty('type'))); }
+
+ this._data.belongsTo[name] = tupleOrReference;
+ },
+
+ _convertTuplesToReferences: function(tuplesOrReferences) {
+ return map(tuplesOrReferences, function(tupleOrReference) {
+ return this._convertTupleToReference(tupleOrReference);
+ }, this);
+ },
+
+ _convertTupleToReference: function(tupleOrReference) {
+ var store = get(this, 'store');
+ if(tupleOrReference.clientId) {
+ return tupleOrReference;
+ } else {
+ return store.referenceForId(tupleOrReference.type, tupleOrReference.id);
+ }
+ },
+
+ rollback: function() {
+ this._setup();
+ this.send('becameClean');
+
+ this.suspendRelationshipObservers(function() {
+ this.notifyPropertyChange('data');
+ });
+ },
+
+ toStringExtension: function() {
+ return get(this, 'id');
+ },
+
+ /**
+ @private
+
+ The goal of this method is to temporarily disable specific observers
+ that take action in response to application changes.
+
+ This allows the system to make changes (such as materialization and
+ rollback) that should not trigger secondary behavior (such as setting an
+ inverse relationship or marking records as dirty).
+
+ The specific implementation will likely change as Ember proper provides
+ better infrastructure for suspending groups of observers, and if Array
+ observation becomes more unified with regular observers.
+ */
+ suspendRelationshipObservers: function(callback, binding) {
+ var observers = get(this.constructor, 'relationshipNames').belongsTo;
+ var self = this;
+
+ try {
+ this._suspendedRelationships = true;
+ Ember._suspendObservers(self, observers, null, 'belongsToDidChange', function() {
+ Ember._suspendBeforeObservers(self, observers, null, 'belongsToWillChange', function() {
+ callback.call(binding || self);
+ });
+ });
+ } finally {
+ this._suspendedRelationships = false;
+ }
+ },
+
+ becameInFlight: function() {
+ },
+
+ /**
+ @private
+
+ */
+ resolveOn: function(successEvent) {
+ var model = this;
+
+ return new Ember.RSVP.Promise(function(resolve, reject) {
+ function success() {
+ this.off('becameError', error);
+ this.off('becameInvalid', error);
+ resolve(this);
+ }
+ function error() {
+ this.off(successEvent, success);
+ reject(this);
+ }
+
+ model.one(successEvent, success);
+ model.one('becameError', error);
+ model.one('becameInvalid', error);
+ });
+ },
+
+ /**
+ Save the record.
+
+ @method save
+ */
+ save: function() {
+ this.get('store').scheduleSave(this);
+
+ return this.resolveOn('didCommit');
+ },
+
+ /**
+ 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`).
+
+ @method reload
+ */
+ reload: function() {
+ this.send('reloadRecord');
+
+ return this.resolveOn('didReload');
+ },
+
+ // 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) {
+ get(this, 'data.attributes')[attributeName] = value;
+ this.notifyPropertyChange(attributeName);
+ } else {
+ value = get(this, attributeName);
+ get(this, 'data.attributes')[attributeName] = value;
+ }
+
+ this.updateRecordArraysLater();
+ },
+
+ adapterDidInvalidate: function(errors) {
+ this.send('becameInvalid', errors);
+ },
+
+ adapterDidError: function() {
+ this.send('becameError');
+ },
+
+ /**
+ @private
+
+ Override the default event firing from Ember.Evented to
+ also call methods with the given name.
+ */
+ trigger: function(name) {
+ Ember.tryInvoke(this, name, [].slice.call(arguments, 1));
+ this._super.apply(this, arguments);
+ }
+});
+
+// Helper function to generate store aliases.
+// This returns a function that invokes the named alias
+// on the default store, but injects the class as the
+// first parameter.
+var storeAlias = function(methodName) {
+ return function() {
+ var store = get(DS, 'defaultStore'),
+ args = [].slice.call(arguments);
+
+ args.unshift(this);
+ Ember.assert("Your application does not have a 'Store' property defined. Attempts to call '" + methodName + "' on model classes will fail. Please provide one as with 'YourAppName.Store = DS.Store.extend()'", !!store);
+ return store[methodName].apply(store, args);
+ };
+};
+
+DS.Model.reopenClass({
+
+ /** @private
+ 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.
+ */
+ _create: DS.Model.create,
+
+ /** @private
+
+ 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.
+ */
+ create: function() {
+ throw new Ember.Error("You should not call `create` on a model. Instead, call `createRecord` with the attributes you would like to set.");
+ },
+
+ /**
+ See {{#crossLink "DS.Store/find:method"}}`DS.Store.find()`{{/crossLink}}.
+
+ @method find
+ @param {Object|String|Array|null} query A query to find records by.
+
+ */
+ find: storeAlias('find'),
+
+ /**
+ See {{#crossLink "DS.Store/all:method"}}`DS.Store.all()`{{/crossLink}}.
+
+ @method all
+ @return {DS.RecordArray}
+ */
+ all: storeAlias('all'),
+
+ /**
+ See {{#crossLink "DS.Store/findQuery:method"}}`DS.Store.findQuery()`{{/crossLink}}.
+
+ @method query
+ @param {Object} query an opaque query to be used by the adapter
+ @return {DS.AdapterPopulatedRecordArray}
+ */
+ query: storeAlias('findQuery'),
+
+ /**
+ See {{#crossLink "DS.Store/filter:method"}}`DS.Store.filter()`{{/crossLink}}.
+
+ @method filter
+ @param {Function} filter
+ @return {DS.FilteredRecordArray}
+ */
+ filter: storeAlias('filter'),
+
+ /**
+ See {{#crossLink "DS.Store/createRecord:method"}}`DS.Store.createRecord()`{{/crossLink}}.
+
+ @method createRecord
+ @param {Object} properties a hash of properties to set on the
+ newly created record.
+ @returns DS.Model
+ */
+ createRecord: storeAlias('createRecord')
+});
+
+})();
+
+
+
+(function() {
+/**
+ @module data
+ @submodule data-model
+*/
+
+var get = Ember.get;
+
+DS.Model.reopenClass({
+ attributes: Ember.computed(function() {
+ var map = Ember.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;
+ })
+});
+
+
+DS.Model.reopen({
+ eachAttribute: function(callback, binding) {
+ get(this.constructor, 'attributes').forEach(function(name, meta) {
+ callback.call(binding, name, meta);
+ }, binding);
+ },
+
+ attributeWillChange: Ember.beforeObserver(function(record, key) {
+ var reference = get(record, '_reference'),
+ store = get(record, 'store');
+
+ record.send('willSetProperty', { reference: reference, store: store, name: key });
+ }),
+
+ attributeDidChange: Ember.observer(function(record, key) {
+ record.send('didSetProperty', { name: key });
+ })
+});
+
+function getAttr(record, options, key) {
+ var attributes = get(record, 'data').attributes;
+ var value = attributes[key];
+
+ if (value === undefined) {
+ if (typeof options.defaultValue === "function") {
+ value = options.defaultValue();
+ } else {
+ value = options.defaultValue;
+ }
+ }
+
+ return value;
+}
+
+DS.attr = function(type, options) {
+ options = options || {};
+
+ var meta = {
+ type: type,
+ isAttribute: true,
+ options: options
+ };
+
+ return Ember.computed(function(key, value, oldValue) {
+ 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');
+ } else {
+ value = getAttr(this, options, key);
+ }
+
+ return value;
+ // `data` is never set directly. However, it may be
+ // invalidated from the state manager's setData
+ // event.
+ }).property('data').meta(meta);
+};
+
+
+})();
+
+
+
+(function() {
+/**
+ @module data
+ @submodule data-model
+*/
+
+})();
+
+
+
+(function() {
+/**
+ An AttributeChange object is created whenever a record's
+ attribute changes value. It is used to track changes to a
+ record between transaction commits.
+*/
+
+var AttributeChange = DS.AttributeChange = function(options) {
+ this.reference = options.reference;
+ this.store = options.store;
+ this.name = options.name;
+ this.oldValue = options.oldValue;
+};
+
+AttributeChange.createChange = function(options) {
+ return new AttributeChange(options);
+};
+
+AttributeChange.prototype = {
+ sync: function() {
+ this.store.recordAttributeDidChange(this.reference, this.name, this.value, this.oldValue);
+
+ // TODO: Use this object in the commit process
+ this.destroy();
+ },
+
+ /**
+ If the AttributeChange is destroyed (either by being rolled back
+ or being committed), remove it from the list of pending changes
+ on the record.
+ */
+ destroy: function() {
+ var record = this.reference.record;
+
+ delete record._changesToSync[this.name];
+ }
+};
+
+})();
+
+
+
+(function() {
+var get = Ember.get, set = Ember.set;
+var forEach = Ember.EnumerableUtils.forEach;
+
+DS.RelationshipChange = function(options) {
+ this.parentReference = options.parentReference;
+ this.childReference = options.childReference;
+ this.firstRecordReference = options.firstRecordReference;
+ this.firstRecordKind = options.firstRecordKind;
+ this.firstRecordName = options.firstRecordName;
+ this.secondRecordReference = options.secondRecordReference;
+ this.secondRecordKind = options.secondRecordKind;
+ this.secondRecordName = options.secondRecordName;
+ this.changeType = options.changeType;
+ this.store = options.store;
+
+ this.committed = {};
+};
+
+DS.RelationshipChangeAdd = function(options){
+ DS.RelationshipChange.call(this, options);
+};
+
+DS.RelationshipChangeRemove = function(options){
+ DS.RelationshipChange.call(this, options);
+};
+
+/** @private */
+DS.RelationshipChange.create = function(options) {
+ return new DS.RelationshipChange(options);
+};
+
+/** @private */
+DS.RelationshipChangeAdd.create = function(options) {
+ return new DS.RelationshipChangeAdd(options);
+};
+
+/** @private */
+DS.RelationshipChangeRemove.create = function(options) {
+ return new DS.RelationshipChangeRemove(options);
+};
+
+DS.OneToManyChange = {};
+DS.OneToNoneChange = {};
+DS.ManyToNoneChange = {};
+DS.OneToOneChange = {};
+DS.ManyToManyChange = {};
+
+DS.RelationshipChange._createChange = function(options){
+ if(options.changeType === "add"){
+ return DS.RelationshipChangeAdd.create(options);
+ }
+ if(options.changeType === "remove"){
+ return DS.RelationshipChangeRemove.create(options);
+ }
+};
+
+
+DS.RelationshipChange.determineRelationshipType = function(recordType, knownSide){
+ var knownKey = knownSide.key, key, otherKind;
+ var knownKind = knownSide.kind;
+
+ var inverse = recordType.inverseFor(knownKey);
+
+ if (inverse){
+ key = inverse.name;
+ otherKind = inverse.kind;
+ }
+
+ if (!inverse){
+ return knownKind === "belongsTo" ? "oneToNone" : "manyToNone";
+ }
+ else{
+ if(otherKind === "belongsTo"){
+ return knownKind === "belongsTo" ? "oneToOne" : "manyToOne";
+ }
+ else{
+ return knownKind === "belongsTo" ? "oneToMany" : "manyToMany";
+ }
+ }
+
+};
+
+DS.RelationshipChange.createChange = function(firstRecordReference, secondRecordReference, store, options){
+ // Get the type of the child based on the child's client ID
+ var firstRecordType = firstRecordReference.type, changeType;
+ changeType = DS.RelationshipChange.determineRelationshipType(firstRecordType, options);
+ if (changeType === "oneToMany"){
+ return DS.OneToManyChange.createChange(firstRecordReference, secondRecordReference, store, options);
+ }
+ else if (changeType === "manyToOne"){
+ return DS.OneToManyChange.createChange(secondRecordReference, firstRecordReference, store, options);
+ }
+ else if (changeType === "oneToNone"){
+ return DS.OneToNoneChange.createChange(firstRecordReference, secondRecordReference, store, options);
+ }
+ else if (changeType === "manyToNone"){
+ return DS.ManyToNoneChange.createChange(firstRecordReference, secondRecordReference, store, options);
+ }
+ else if (changeType === "oneToOne"){
+ return DS.OneToOneChange.createChange(firstRecordReference, secondRecordReference, store, options);
+ }
+ else if (changeType === "manyToMany"){
+ return DS.ManyToManyChange.createChange(firstRecordReference, secondRecordReference, store, options);
+ }
+};
+
+/** @private */
+DS.OneToNoneChange.createChange = function(childReference, parentReference, store, options) {
+ var key = options.key;
+ var change = DS.RelationshipChange._createChange({
+ parentReference: parentReference,
+ childReference: childReference,
+ firstRecordReference: childReference,
+ store: store,
+ changeType: options.changeType,
+ firstRecordName: key,
+ firstRecordKind: "belongsTo"
+ });
+
+ store.addRelationshipChangeFor(childReference, key, parentReference, null, change);
+
+ return change;
+};
+
+/** @private */
+DS.ManyToNoneChange.createChange = function(childReference, parentReference, store, options) {
+ var key = options.key;
+ var change = DS.RelationshipChange._createChange({
+ parentReference: childReference,
+ childReference: parentReference,
+ secondRecordReference: childReference,
+ store: store,
+ changeType: options.changeType,
+ secondRecordName: options.key,
+ secondRecordKind: "hasMany"
+ });
+
+ store.addRelationshipChangeFor(childReference, key, parentReference, null, change);
+ return change;
+};
+
+
+/** @private */
+DS.ManyToManyChange.createChange = function(childReference, parentReference, store, options) {
+ // If the name of the belongsTo side of the relationship is specified,
+ // use that
+ // If the type of the parent is specified, look it up on the child's type
+ // definition.
+ var key = options.key;
+
+ var change = DS.RelationshipChange._createChange({
+ parentReference: parentReference,
+ childReference: childReference,
+ firstRecordReference: childReference,
+ secondRecordReference: parentReference,
+ firstRecordKind: "hasMany",
+ secondRecordKind: "hasMany",
+ store: store,
+ changeType: options.changeType,
+ firstRecordName: key
+ });
+
+ store.addRelationshipChangeFor(childReference, key, parentReference, null, change);
+
+
+ return change;
+};
+
+/** @private */
+DS.OneToOneChange.createChange = function(childReference, parentReference, store, options) {
+ var key;
+
+ // If the name of the belongsTo side of the relationship is specified,
+ // use that
+ // If the type of the parent is specified, look it up on the child's type
+ // definition.
+ if (options.parentType) {
+ key = options.parentType.inverseFor(options.key).name;
+ } else if (options.key) {
+ key = options.key;
+ } else {
+ Ember.assert("You must pass either a parentType or belongsToName option to OneToManyChange.forChildAndParent", false);
+ }
+
+ var change = DS.RelationshipChange._createChange({
+ parentReference: parentReference,
+ childReference: childReference,
+ firstRecordReference: childReference,
+ secondRecordReference: parentReference,
+ firstRecordKind: "belongsTo",
+ secondRecordKind: "belongsTo",
+ store: store,
+ changeType: options.changeType,
+ firstRecordName: key
+ });
+
+ store.addRelationshipChangeFor(childReference, key, parentReference, null, change);
+
+
+ return change;
+};
+
+DS.OneToOneChange.maintainInvariant = function(options, store, childReference, key){
+ if (options.changeType === "add" && store.recordIsMaterialized(childReference)) {
+ var child = store.recordForReference(childReference);
+ var oldParent = get(child, key);
+ if (oldParent){
+ var correspondingChange = DS.OneToOneChange.createChange(childReference, oldParent.get('_reference'), store, {
+ parentType: options.parentType,
+ hasManyName: options.hasManyName,
+ changeType: "remove",
+ key: options.key
+ });
+ store.addRelationshipChangeFor(childReference, key, options.parentReference , null, correspondingChange);
+ correspondingChange.sync();
+ }
+ }
+};
+
+/** @private */
+DS.OneToManyChange.createChange = function(childReference, parentReference, store, options) {
+ var key;
+
+ // If the name of the belongsTo side of the relationship is specified,
+ // use that
+ // If the type of the parent is specified, look it up on the child's type
+ // definition.
+ if (options.parentType) {
+ key = options.parentType.inverseFor(options.key).name;
+ DS.OneToManyChange.maintainInvariant( options, store, childReference, key );
+ } else if (options.key) {
+ key = options.key;
+ } else {
+ Ember.assert("You must pass either a parentType or belongsToName option to OneToManyChange.forChildAndParent", false);
+ }
+
+ var change = DS.RelationshipChange._createChange({
+ parentReference: parentReference,
+ childReference: childReference,
+ firstRecordReference: childReference,
+ secondRecordReference: parentReference,
+ firstRecordKind: "belongsTo",
+ secondRecordKind: "hasMany",
+ store: store,
+ changeType: options.changeType,
+ firstRecordName: key
+ });
+
+ store.addRelationshipChangeFor(childReference, key, parentReference, change.getSecondRecordName(), change);
+
+
+ return change;
+};
+
+
+DS.OneToManyChange.maintainInvariant = function(options, store, childReference, key){
+ var child = childReference.record;
+
+ if (options.changeType === "add" && child) {
+ var oldParent = get(child, key);
+ if (oldParent){
+ var correspondingChange = DS.OneToManyChange.createChange(childReference, oldParent.get('_reference'), store, {
+ parentType: options.parentType,
+ hasManyName: options.hasManyName,
+ changeType: "remove",
+ key: options.key
+ });
+ store.addRelationshipChangeFor(childReference, key, options.parentReference, correspondingChange.getSecondRecordName(), correspondingChange);
+ correspondingChange.sync();
+ }
+ }
+};
+
+DS.OneToManyChange.ensureSameTransaction = function(changes){
+ var records = Ember.A();
+ forEach(changes, function(change){
+ records.addObject(change.getSecondRecord());
+ records.addObject(change.getFirstRecord());
+ });
+
+ return DS.Transaction.ensureSameTransaction(records);
+};
+
+DS.RelationshipChange.prototype = {
+
+ getSecondRecordName: function() {
+ var name = this.secondRecordName, parent;
+
+ if (!name) {
+ parent = this.secondRecordReference;
+ if (!parent) { return; }
+
+ var childType = this.firstRecordReference.type;
+ var inverse = childType.inverseFor(this.firstRecordName);
+ this.secondRecordName = inverse.name;
+ }
+
+ return this.secondRecordName;
+ },
+
+ /**
+ Get the name of the relationship on the belongsTo side.
+
+ @return {String}
+ */
+ getFirstRecordName: function() {
+ var name = this.firstRecordName;
+ return name;
+ },
+
+ /** @private */
+ destroy: function() {
+ var childReference = this.childReference,
+ belongsToName = this.getFirstRecordName(),
+ hasManyName = this.getSecondRecordName(),
+ store = this.store;
+
+ store.removeRelationshipChangeFor(childReference, belongsToName, this.parentReference, hasManyName, this.changeType);
+ },
+
+ /** @private */
+ getByReference: function(reference) {
+ // return null or undefined if the original reference was null or undefined
+ if (!reference) { return reference; }
+
+ if (reference.record) {
+ return reference.record;
+ }
+ },
+
+ getSecondRecord: function(){
+ return this.getByReference(this.secondRecordReference);
+ },
+
+ /** @private */
+ getFirstRecord: function() {
+ return this.getByReference(this.firstRecordReference);
+ },
+
+ /**
+ @private
+
+ Make sure that all three parts of the relationship change are part of
+ the same transaction. If any of the three records is clean and in the
+ default transaction, and the rest are in a different transaction, move
+ them all into that transaction.
+ */
+ ensureSameTransaction: function() {
+ var child = this.getFirstRecord(),
+ parentRecord = this.getSecondRecord();
+
+ var transaction = DS.Transaction.ensureSameTransaction([child, parentRecord]);
+
+ this.transaction = transaction;
+ return transaction;
+ },
+
+ callChangeEvents: function(){
+ var child = this.getFirstRecord(),
+ parentRecord = this.getSecondRecord();
+
+ var dirtySet = new Ember.OrderedSet();
+
+ // TODO: This implementation causes a race condition in key-value
+ // stores. The fix involves buffering changes that happen while
+ // a record is loading. A similar fix is required for other parts
+ // of ember-data, and should be done as new infrastructure, not
+ // a one-off hack. [tomhuda]
+ if (parentRecord && get(parentRecord, 'isLoaded')) {
+ this.store.recordHasManyDidChange(dirtySet, parentRecord, this);
+ }
+
+ if (child) {
+ this.store.recordBelongsToDidChange(dirtySet, child, this);
+ }
+
+ dirtySet.forEach(function(record) {
+ record.adapterDidDirty();
+ });
+ },
+
+ coalesce: function(){
+ var relationshipPairs = this.store.relationshipChangePairsFor(this.firstRecordReference);
+ forEach(relationshipPairs, function(pair){
+ var addedChange = pair["add"];
+ var removedChange = pair["remove"];
+ if(addedChange && removedChange) {
+ addedChange.destroy();
+ removedChange.destroy();
+ }
+ });
+ }
+};
+
+DS.RelationshipChangeAdd.prototype = Ember.create(DS.RelationshipChange.create({}));
+DS.RelationshipChangeRemove.prototype = Ember.create(DS.RelationshipChange.create({}));
+
+DS.RelationshipChangeAdd.prototype.changeType = "add";
+DS.RelationshipChangeAdd.prototype.sync = function() {
+ var secondRecordName = this.getSecondRecordName(),
+ firstRecordName = this.getFirstRecordName(),
+ firstRecord = this.getFirstRecord(),
+ secondRecord = this.getSecondRecord();
+
+ //Ember.assert("You specified a hasMany (" + hasManyName + ") on " + (!belongsToName && (newParent || oldParent || this.lastParent).constructor) + " but did not specify an inverse belongsTo on " + child.constructor, belongsToName);
+ //Ember.assert("You specified a belongsTo (" + belongsToName + ") on " + child.constructor + " but did not specify an inverse hasMany on " + (!hasManyName && (newParent || oldParent || this.lastParentRecord).constructor), hasManyName);
+
+ this.ensureSameTransaction();
+
+ this.callChangeEvents();
+
+ if (secondRecord && firstRecord) {
+ if(this.secondRecordKind === "belongsTo"){
+ secondRecord.suspendRelationshipObservers(function(){
+ set(secondRecord, secondRecordName, firstRecord);
+ });
+
+ }
+ else if(this.secondRecordKind === "hasMany"){
+ secondRecord.suspendRelationshipObservers(function(){
+ get(secondRecord, secondRecordName).addObject(firstRecord);
+ });
+ }
+ }
+
+ if (firstRecord && secondRecord && get(firstRecord, firstRecordName) !== secondRecord) {
+ if(this.firstRecordKind === "belongsTo"){
+ firstRecord.suspendRelationshipObservers(function(){
+ set(firstRecord, firstRecordName, secondRecord);
+ });
+ }
+ else if(this.firstRecordKind === "hasMany"){
+ firstRecord.suspendRelationshipObservers(function(){
+ get(firstRecord, firstRecordName).addObject(secondRecord);
+ });
+ }
+ }
+
+ this.coalesce();
+};
+
+DS.RelationshipChangeRemove.prototype.changeType = "remove";
+DS.RelationshipChangeRemove.prototype.sync = function() {
+ var secondRecordName = this.getSecondRecordName(),
+ firstRecordName = this.getFirstRecordName(),
+ firstRecord = this.getFirstRecord(),
+ secondRecord = this.getSecondRecord();
+
+ //Ember.assert("You specified a hasMany (" + hasManyName + ") on " + (!belongsToName && (newParent || oldParent || this.lastParent).constructor) + " but did not specify an inverse belongsTo on " + child.constructor, belongsToName);
+ //Ember.assert("You specified a belongsTo (" + belongsToName + ") on " + child.constructor + " but did not specify an inverse hasMany on " + (!hasManyName && (newParent || oldParent || this.lastParentRecord).constructor), hasManyName);
+
+ this.ensureSameTransaction(firstRecord, secondRecord, secondRecordName, firstRecordName);
+
+ this.callChangeEvents();
+
+ if (secondRecord && firstRecord) {
+ if(this.secondRecordKind === "belongsTo"){
+ secondRecord.suspendRelationshipObservers(function(){
+ set(secondRecord, secondRecordName, null);
+ });
+ }
+ else if(this.secondRecordKind === "hasMany"){
+ secondRecord.suspendRelationshipObservers(function(){
+ get(secondRecord, secondRecordName).removeObject(firstRecord);
+ });
+ }
+ }
+
+ if (firstRecord && get(firstRecord, firstRecordName)) {
+ if(this.firstRecordKind === "belongsTo"){
+ firstRecord.suspendRelationshipObservers(function(){
+ set(firstRecord, firstRecordName, null);
+ });
+ }
+ else if(this.firstRecordKind === "hasMany"){
+ firstRecord.suspendRelationshipObservers(function(){
+ get(firstRecord, firstRecordName).removeObject(secondRecord);
+ });
+ }
+ }
+
+ this.coalesce();
+};
+
+})();
+
+
+
+(function() {
+/**
+ @module data
+ @submodule data-changes
+*/
+
+})();
+
+
+
+(function() {
+var get = Ember.get, set = Ember.set,
+ isNone = Ember.isNone;
+
+DS.belongsTo = function(type, options) {
+ Ember.assert("The first argument DS.belongsTo must be a model type or string, like DS.belongsTo(App.Person)", !!type && (typeof type === 'string' || DS.Model.detect(type)));
+
+ options = options || {};
+
+ var meta = { type: type, isRelationship: true, options: options, kind: 'belongsTo' };
+
+ return Ember.computed(function(key, value) {
+ if (typeof type === 'string') {
+ type = get(this, type, false) || get(Ember.lookup, type);
+ }
+
+ if (arguments.length === 2) {
+ Ember.assert("You can only add a record of " + type.toString() + " to this relationship", !value || type.detectInstance(value));
+ return value === undefined ? null : value;
+ }
+
+ var data = get(this, 'data').belongsTo,
+ store = get(this, 'store'), belongsTo;
+
+ belongsTo = data[key];
+
+ // TODO (tomdale) The value of the belongsTo in the data hash can be
+ // one of:
+ // 1. null/undefined
+ // 2. a record reference
+ // 3. a tuple returned by the serializer's polymorphism code
+ //
+ // We should really normalize #3 to be the same as #2 to reduce the
+ // complexity here.
+
+ if (isNone(belongsTo)) {
+ return null;
+ }
+
+ // The data has been normalized to a record reference, so
+ // just ask the store for the record for that reference,
+ // materializing it if necessary.
+ if (belongsTo.clientId) {
+ return store.recordForReference(belongsTo);
+ }
+
+ // The data has been normalized into a type/id pair by the
+ // serializer's polymorphism code.
+ return store.findById(belongsTo.type, belongsTo.id);
+ }).property('data').meta(meta);
+};
+
+/**
+ These observers observe all `belongsTo` relationships on the record. See
+ `relationships/ext` to see how these observers get their dependencies.
+
+*/
+
+DS.Model.reopen({
+ /** @private */
+ belongsToWillChange: Ember.beforeObserver(function(record, key) {
+ if (get(record, 'isLoaded')) {
+ var oldParent = get(record, key);
+
+ var childReference = get(record, '_reference'),
+ store = get(record, 'store');
+ if (oldParent){
+ var change = DS.RelationshipChange.createChange(childReference, get(oldParent, '_reference'), store, { key: key, kind:"belongsTo", changeType: "remove" });
+ change.sync();
+ this._changesToSync[key] = change;
+ }
+ }
+ }),
+
+ /** @private */
+ belongsToDidChange: Ember.immediateObserver(function(record, key) {
+ if (get(record, 'isLoaded')) {
+ var newParent = get(record, key);
+ if(newParent){
+ var childReference = get(record, '_reference'),
+ store = get(record, 'store');
+ var change = DS.RelationshipChange.createChange(childReference, get(newParent, '_reference'), store, { key: key, kind:"belongsTo", changeType: "add" });
+ change.sync();
+ if(this._changesToSync[key]){
+ DS.OneToManyChange.ensureSameTransaction([change, this._changesToSync[key]], store);
+ }
+ }
+ }
+ delete this._changesToSync[key];
+ })
+});
+
+})();
+
+
+
+(function() {
+var get = Ember.get, set = Ember.set, forEach = Ember.EnumerableUtils.forEach;
+var hasRelationship = function(type, options) {
+ options = options || {};
+
+ var meta = { type: type, isRelationship: true, options: options, kind: 'hasMany' };
+
+ return Ember.computed(function(key, value) {
+ var data = get(this, 'data').hasMany,
+ store = get(this, 'store'),
+ ids, relationship;
+
+ if (typeof type === 'string') {
+ type = get(this, type, false) || get(Ember.lookup, type);
+ }
+
+ //ids can be references or opaque token
+ //(e.g. `{url: '/relationship'}`) that will be passed to the adapter
+ ids = data[key];
+
+ relationship = store.findMany(type, ids, this, meta);
+ set(relationship, 'owner', this);
+ set(relationship, 'name', key);
+ set(relationship, 'isPolymorphic', options.polymorphic);
+
+ return relationship;
+ }).property().meta(meta);
+};
+
+DS.hasMany = function(type, options) {
+ Ember.assert("The type passed to DS.hasMany must be defined", !!type);
+ return hasRelationship(type, options);
+};
+
+function clearUnmaterializedHasMany(record, relationship) {
+ var data = get(record, 'data').hasMany;
+
+ var references = data[relationship.key];
+
+ if (!references) { return; }
+
+ var inverse = record.constructor.inverseFor(relationship.key);
+
+ if (inverse) {
+ forEach(references, function(reference) {
+ var childRecord;
+
+ if (childRecord = reference.record) {
+ record.suspendRelationshipObservers(function() {
+ set(childRecord, inverse.name, null);
+ });
+ }
+ });
+ }
+}
+
+DS.Model.reopen({
+ clearHasMany: function(relationship) {
+ var hasMany = this.cacheFor(relationship.name);
+
+ if (hasMany) {
+ hasMany.clear();
+ } else {
+ clearUnmaterializedHasMany(this, relationship);
+ }
+ }
+});
+
+})();
+
+
+
+(function() {
+var get = Ember.get, set = Ember.set;
+
+/**
+ @private
+
+ This file defines several extensions to the base `DS.Model` class that
+ add support for one-to-many relationships.
+*/
+
+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:
+ //
+ // DS.Model.extend({
+ // parent: DS.belongsTo(App.User)
+ // });
+ //
+ // This hook would be called with "parent" as the key and the computed
+ // property returned by `DS.belongsTo` as the value.
+ didDefineProperty: function(proto, key, value) {
+ // Check if the value being set is a computed property.
+ if (value instanceof Ember.Descriptor) {
+
+ // 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();
+
+ if (meta.isRelationship && meta.kind === 'belongsTo') {
+ Ember.addObserver(proto, key, null, 'belongsToDidChange');
+ Ember.addBeforeObserver(proto, key, null, 'belongsToWillChange');
+ }
+
+ if (meta.isAttribute) {
+ Ember.addObserver(proto, key, null, 'attributeDidChange');
+ Ember.addBeforeObserver(proto, key, null, 'attributeWillChange');
+ }
+
+ 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.
+*/
+
+DS.Model.reopenClass({
+ /**
+ For a given relationship name, returns the model type of the relationship.
+
+ For example, if you define a model like this:
+
+ App.Post = DS.Model.extend({
+ comments: DS.hasMany(App.Comment)
+ });
+
+ Calling `App.Post.typeForRelationship('comments')` will return `App.Comment`.
+
+ @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;
+ },
+
+ inverseFor: function(name) {
+ var inverseType = this.typeForRelationship(name);
+
+ if (!inverseType) { return null; }
+
+ var options = this.metaForProperty(name).options;
+ var inverseName, inverseKind;
+
+ if (options.inverse) {
+ inverseName = options.inverse;
+ inverseKind = Ember.get(inverseType, 'relationshipsByName').get(inverseName).kind;
+ } else {
+ var possibleRelationships = findPossibleInverses(this, inverseType);
+
+ if (possibleRelationships.length === 0) { return null; }
+
+ Ember.assert("You defined the '" + name + "' relationship on " + this + ", but multiple possible inverse relationships of type " + this + " were found on " + inverseType + ".", possibleRelationships.length === 1);
+
+ inverseName = possibleRelationships[0].name;
+ inverseKind = possibleRelationships[0].kind;
+ }
+
+ function findPossibleInverses(type, inverseType, possibleRelationships) {
+ possibleRelationships = possibleRelationships || [];
+
+ var relationshipMap = get(inverseType, 'relationships');
+ if (!relationshipMap) { return; }
+
+ var relationships = relationshipMap.get(type);
+ if (relationships) {
+ possibleRelationships.push.apply(possibleRelationships, relationshipMap.get(type));
+ }
+
+ 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:
+
+ App.Blog = DS.Model.extend({
+ users: DS.hasMany(App.User),
+ owner: DS.belongsTo(App.User),
+ posts: DS.hasMany(App.Post)
+ });
+
+ This computed property would return a map describing these
+ relationships, like this:
+
+ 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' } ]
+
+ @type Ember.Map
+ @readOnly
+ */
+ relationships: Ember.computed(function() {
+ var map = new Ember.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) {
+ if (typeof meta.type === 'string') {
+ meta.type = Ember.get(Ember.lookup, meta.type);
+ }
+
+ var relationshipsForType = map.get(meta.type);
+
+ relationshipsForType.push({ name: name, kind: meta.kind });
+ }
+ });
+
+ return map;
+ }),
+
+ /**
+ A hash containing lists of the model's relationships, grouped
+ by the relationship kind. For example, given a model with this
+ definition:
+
+ App.Blog = DS.Model.extend({
+ users: DS.hasMany(App.User),
+ owner: DS.belongsTo(App.User),
+
+ posts: DS.hasMany(App.Post)
+ });
+
+ This property would contain the following:
+
+ var relationshipNames = Ember.get(App.Blog, 'relationshipNames');
+ relationshipNames.hasMany;
+ //=> ['users', 'posts']
+ relationshipNames.belongsTo;
+ //=> ['owner']
+
+ @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:
+
+ App.Blog = DS.Model.extend({
+ users: DS.hasMany(App.User),
+ owner: DS.belongsTo(App.User),
+ posts: DS.hasMany(App.Post)
+ });
+
+ This property would contain the following:
+
+ var relatedTypes = Ember.get(App.Blog, 'relatedTypes');
+ //=> [ App.User, App.Post ]
+
+ @type Ember.Array
+ @readOnly
+ */
+ relatedTypes: Ember.computed(function() {
+ var type,
+ 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) {
+ type = meta.type;
+
+ if (typeof type === 'string') {
+ type = get(this, type, false) || get(Ember.lookup, type);
+ }
+
+ 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;
+ }),
+
+ /**
+ A map whose keys are the relationships of a model and whose values are
+ relationship descriptors.
+
+ For example, given a model with this
+ definition:
+
+ App.Blog = DS.Model.extend({
+ users: DS.hasMany(App.User),
+ owner: DS.belongsTo(App.User),
+
+ posts: DS.hasMany(App.Post)
+ });
+
+ This property would contain the following:
+
+ 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 }
+
+ @type Ember.Map
+ @readOnly
+ */
+ relationshipsByName: Ember.computed(function() {
+ var map = Ember.Map.create(), type;
+
+ this.eachComputedProperty(function(name, meta) {
+ if (meta.isRelationship) {
+ meta.key = name;
+ type = meta.type;
+
+ if (typeof type === 'string') {
+ type = get(this, type, false) || get(Ember.lookup, type);
+ meta.type = type;
+ }
+
+ map.set(name, meta);
+ }
+ });
+
+ return map;
+ }),
+
+ /**
+ 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:
+
+ App.Blog = DS.Model.extend({
+ users: DS.hasMany(App.User),
+ owner: DS.belongsTo(App.User),
+
+ posts: DS.hasMany(App.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
+
+ @type Ember.Map
+ @readOnly
+ */
+ fields: Ember.computed(function() {
+ var map = Ember.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;
+ }),
+
+ /**
+ 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.
+
+ @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(name, relationship) {
+ 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.
+
+ @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);
+ });
+ }
+});
+
+DS.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.
+
+ @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);
+ }
+});
+
+})();
+
+
+
+(function() {
+/**
+ @module data
+ @submodule data-relationships
+*/
+
+})();
+
+
+
+(function() {
+var get = Ember.get, set = Ember.set;
+var once = Ember.run.once;
+var forEach = Ember.EnumerableUtils.forEach;
+
+DS.RecordArrayManager = Ember.Object.extend({
+ init: function() {
+ this.filteredRecordArrays = Ember.MapWithDefault.create({
+ defaultValue: function() { return []; }
+ });
+
+ this.changedReferences = [];
+ },
+
+ referenceDidChange: function(reference) {
+ this.changedReferences.push(reference);
+ once(this, this.updateRecordArrays);
+ },
+
+ recordArraysForReference: function(reference) {
+ reference.recordArrays = reference.recordArrays || Ember.OrderedSet.create();
+ return reference.recordArrays;
+ },
+
+ /**
+ @private
+
+ This method is invoked whenever data is loaded into the store
+ by the adapter or updated by the adapter, or when an attribute
+ changes on a record.
+
+ It updates all filters that a record belongs to.
+
+ To avoid thrashing, it only runs once per run loop per record.
+
+ @param {Class} type
+ @param {Number|String} clientId
+ */
+ updateRecordArrays: function() {
+ forEach(this.changedReferences, function(reference) {
+ var type = reference.type,
+ recordArrays = this.filteredRecordArrays.get(type),
+ filter;
+
+ forEach(recordArrays, function(array) {
+ filter = get(array, 'filterFunction');
+ this.updateRecordArray(array, filter, type, reference);
+ }, this);
+
+ // loop through all manyArrays containing an unloaded copy of this
+ // clientId and notify them that the record was loaded.
+ var manyArrays = reference.loadingRecordArrays;
+
+ if (manyArrays) {
+ for (var i=0, l=manyArrays.length; i<l; i++) {
+ manyArrays[i].loadedRecord();
+ }
+
+ reference.loadingRecordArrays = [];
+ }
+ }, this);
+
+ this.changedReferences = [];
+ },
+
+ /**
+ @private
+
+ Update an individual filter.
+
+ @param {DS.FilteredRecordArray} array
+ @param {Function} filter
+ @param {Class} type
+ @param {Number|String} clientId
+ */
+ updateRecordArray: function(array, filter, type, reference) {
+ var shouldBeInArray, record;
+
+ if (!filter) {
+ shouldBeInArray = true;
+ } else {
+ record = this.store.recordForReference(reference);
+ shouldBeInArray = filter(record);
+ }
+
+ var recordArrays = this.recordArraysForReference(reference);
+
+ if (shouldBeInArray) {
+ recordArrays.add(array);
+ array.addReference(reference);
+ } else if (!shouldBeInArray) {
+ recordArrays.remove(array);
+ array.removeReference(reference);
+ }
+ },
+
+ /**
+ @private
+
+ When a record is deleted, it is removed from all its
+ record arrays.
+
+ @param {DS.Model} record
+ */
+ remove: function(record) {
+ var reference = get(record, '_reference');
+ var recordArrays = reference.recordArrays || [];
+
+ recordArrays.forEach(function(array) {
+ array.removeReference(reference);
+ });
+ },
+
+ /**
+ @private
+
+ 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.
+ */
+ updateFilter: function(array, type, filter) {
+ var typeMap = this.store.typeMapFor(type),
+ references = typeMap.references,
+ reference, data, shouldFilter, record;
+
+ for (var i=0, l=references.length; i<l; i++) {
+ reference = references[i];
+ shouldFilter = false;
+
+ data = reference.data;
+
+ if (typeof data === 'object') {
+ if (record = reference.record) {
+ if (!get(record, 'isDeleted')) { shouldFilter = true; }
+ } else {
+ shouldFilter = true;
+ }
+
+ if (shouldFilter) {
+ this.updateRecordArray(array, filter, type, reference);
+ }
+ }
+ }
+ },
+
+ /**
+ @private
+
+ Create a `DS.ManyArray` for a type and list of record references, and index
+ the `ManyArray` under each reference. This allows us to efficiently remove
+ records from `ManyArray`s when they are deleted.
+
+ @param {Class} type
+ @param {Array} references
+
+ @return {DS.ManyArray}
+ */
+ createManyArray: function(type, references) {
+ var manyArray = DS.ManyArray.create({
+ type: type,
+ content: references,
+ store: this.store
+ });
+
+ references.forEach(function(reference) {
+ var arrays = this.recordArraysForReference(reference);
+ arrays.add(manyArray);
+ }, this);
+
+ return manyArray;
+ },
+
+ /**
+ @private
+
+ Register a RecordArray for a given type to be backed by
+ a filter function. This will cause the array to update
+ automatically when records of that type change attribute
+ values or states.
+
+ @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(array, reference) {
+ var loadingRecordArrays = reference.loadingRecordArrays || [];
+ loadingRecordArrays.push(array);
+ reference.loadingRecordArrays = loadingRecordArrays;
+ }
+});
+
+})();
+
+
+
+(function() {
+var get = Ember.get, set = Ember.set, map = Ember.ArrayPolyfills.map, isNone = Ember.isNone;
+
+function mustImplement(name) {
+ return function() {
+ throw new Ember.Error("Your serializer " + this.toString() + " does not implement the required method " + name);
+ };
+}
+
+/**
+ A serializer is responsible for serializing and deserializing a group of
+ records.
+
+ `DS.Serializer` is an abstract base class designed to help you build a
+ serializer that can read to and write from any serialized form. While most
+ applications will use `DS.JSONSerializer`, which reads and writes JSON, the
+ serializer architecture allows your adapter to transmit things like XML,
+ strings, or custom binary data.
+
+ Typically, your application's `DS.Adapter` is responsible for both creating a
+ serializer as well as calling the appropriate methods when it needs to
+ materialize data or serialize a record.
+
+ The serializer API is designed as a series of layered hooks that you can
+ override to customize any of the individual steps of serialization and
+ deserialization.
+
+ The hooks are organized by the three responsibilities of the serializer:
+
+ 1. Determining naming conventions
+ 2. Serializing records into a serialized form
+ 3. Deserializing records from a serialized form
+
+ Because Ember Data lazily materializes records, the deserialization
+ step, and therefore the hooks you implement, are split into two phases:
+
+ 1. Extraction, where the serialized forms for multiple records are
+ extracted from a single payload. The IDs of each record are also
+ extracted for indexing.
+ 2. Materialization, where a newly-created record has its attributes
+ and relationships initialized based on the serialized form loaded
+ by the adapter.
+
+ Additionally, a serializer can convert values from their JavaScript
+ versions into their serialized versions via a declarative API.
+
+ ## Naming Conventions
+
+ One of the most common uses of the serializer is to map attribute names
+ from the serialized form to your `DS.Model`. For example, in your model,
+ you may have an attribute called `firstName`:
+
+ ```javascript
+ App.Person = DS.Model.extend({
+ firstName: DS.attr('string')
+ });
+ ```
+
+ However, because the web API your adapter is communicating with is
+ legacy, it calls this attribute `FIRST_NAME`.
+
+ You can determine the attribute name used in the serialized form
+ by implementing `keyForAttributeName`:
+
+ ```javascript
+ keyForAttributeName: function(type, name) {
+ return name.underscore.toUpperCase();
+ }
+ ```
+
+ If your attribute names are not predictable, you can re-map them
+ one-by-one using the adapter's `map` API:
+
+ ```javascript
+ App.Adapter.map('App.Person', {
+ firstName: { key: '*API_USER_FIRST_NAME*' }
+ });
+ ```
+
+ This API will also work for relationships and primary keys. For
+ example:
+
+ ```javascript
+ App.Adapter.map('App.Person', {
+ primaryKey: '_id'
+ });
+ ```
+
+ ## Serialization
+
+ During the serialization process, a record or records are converted
+ from Ember.js objects into their serialized form.
+
+ These methods are designed in layers, like a delicious 7-layer
+ cake (but with fewer layers).
+
+ The main entry point for serialization is the `serialize`
+ method, which takes the record and options.
+
+ The `serialize` method is responsible for:
+
+ * turning the record's attributes (`DS.attr`) into
+ attributes on the JSON object.
+ * optionally adding the record's ID onto the hash
+ * adding relationships (`DS.hasMany` and `DS.belongsTo`)
+ to the JSON object.
+
+ Depending on the backend, the serializer can choose
+ whether to include the `hasMany` or `belongsTo`
+ relationships on the JSON hash.
+
+ For very custom serialization, you can implement your
+ own `serialize` method. In general, however, you will want
+ to override the hooks described below.
+
+ ### Adding the ID
+
+ The default `serialize` will optionally call your serializer's
+ `addId` method with the JSON hash it is creating, the
+ record's type, and the record's ID. The `serialize` method
+ will not call `addId` if the record's ID is undefined.
+
+ Your adapter must specifically request ID inclusion by
+ passing `{ includeId: true }` as an option to `serialize`.
+
+ NOTE: You may not want to include the ID when updating an
+ existing record, because your server will likely disallow
+ changing an ID after it is created, and the PUT request
+ itself will include the record's identification.
+
+ By default, `addId` will:
+
+ 1. Get the primary key name for the record by calling
+ the serializer's `primaryKey` with the record's type.
+ Unless you override the `primaryKey` method, this
+ will be `'id'`.
+ 2. Assign the record's ID to the primary key in the
+ JSON hash being built.
+
+ If your backend expects a JSON object with the primary
+ key at the root, you can just override the `primaryKey`
+ method on your serializer subclass.
+
+ Otherwise, you can override the `addId` method for
+ more specialized handling.
+
+ ### Adding Attributes
+
+ By default, the serializer's `serialize` method will call
+ `addAttributes` with the JSON object it is creating
+ and the record to serialize.
+
+ The `addAttributes` method will then call `addAttribute`
+ in turn, with the JSON object, the record to serialize,
+ the attribute's name and its type.
+
+ Finally, the `addAttribute` method will serialize the
+ attribute:
+
+ 1. It will call `keyForAttributeName` to determine
+ the key to use in the JSON hash.
+ 2. It will get the value from the record.
+ 3. It will call `serializeValue` with the attribute's
+ value and attribute type to convert it into a
+ JSON-compatible value. For example, it will convert a
+ Date into a String.
+
+ If your backend expects a JSON object with attributes as
+ keys at the root, you can just override the `serializeValue`
+ and `keyForAttributeName` methods in your serializer
+ subclass and let the base class do the heavy lifting.
+
+ If you need something more specialized, you can probably
+ override `addAttribute` and let the default `addAttributes`
+ handle the nitty gritty.
+
+ ### Adding Relationships
+
+ By default, `serialize` will call your serializer's
+ `addRelationships` method with the JSON object that is
+ being built and the record being serialized. The default
+ implementation of this method is to loop over all of the
+ relationships defined on your record type and:
+
+ * If the relationship is a `DS.hasMany` relationship,
+ call `addHasMany` with the JSON object, the record
+ and a description of the relationship.
+ * If the relationship is a `DS.belongsTo` relationship,
+ call `addBelongsTo` with the JSON object, the record
+ and a description of the relationship.
+
+ The relationship description has the following keys:
+
+ * `type`: the class of the associated information (the
+ first parameter to `DS.hasMany` or `DS.belongsTo`)
+ * `kind`: either `hasMany` or `belongsTo`
+
+ The relationship description may get additional
+ information in the future if more capabilities or
+ relationship types are added. However, it will
+ remain backwards-compatible, so the mere existence
+ of new features should not break existing adapters.
+
+ @module data
+ @submodule data-serializer
+ @main data-serializer
+
+ @class Serializer
+ @namespace DS
+ @extends Ember.Object
+ @constructor
+*/
+
+DS.Serializer = Ember.Object.extend({
+ init: function() {
+ this.mappings = Ember.Map.create();
+ this.aliases = Ember.Map.create();
+ this.configurations = Ember.Map.create();
+ this.globalConfigurations = {};
+ },
+
+ extract: mustImplement('extract'),
+ extractMany: mustImplement('extractMany'),
+ extractId: mustImplement('extractId'),
+ extractAttribute: mustImplement('extractAttribute'),
+ extractHasMany: mustImplement('extractHasMany'),
+ extractBelongsTo: mustImplement('extractBelongsTo'),
+
+ extractRecordRepresentation: function(loader, type, data, shouldSideload) {
+ var prematerialized = {}, reference;
+
+ if (shouldSideload) {
+ reference = loader.sideload(type, data);
+ } else {
+ reference = loader.load(type, data);
+ }
+
+ this.eachEmbeddedHasMany(type, function(name, relationship) {
+ var embeddedData = this.extractEmbeddedData(data, this.keyFor(relationship));
+ if (!isNone(embeddedData)) {
+ this.extractEmbeddedHasMany(loader, relationship, embeddedData, reference, prematerialized);
+ }
+ }, this);
+
+ this.eachEmbeddedBelongsTo(type, function(name, relationship) {
+ var embeddedData = this.extractEmbeddedData(data, this.keyFor(relationship));
+ if (!isNone(embeddedData)) {
+ this.extractEmbeddedBelongsTo(loader, relationship, embeddedData, reference, prematerialized);
+ }
+ }, this);
+
+ loader.prematerialize(reference, prematerialized);
+
+ return reference;
+ },
+
+ extractEmbeddedHasMany: function(loader, relationship, array, parent, prematerialized) {
+ var references = map.call(array, function(item) {
+ if (!item) { return; }
+
+ var foundType = this.extractEmbeddedType(relationship, item),
+ reference = this.extractRecordRepresentation(loader, foundType, item, true);
+
+ // If the embedded record should also be saved back when serializing the parent,
+ // make sure we set its parent since it will not have an ID.
+ var embeddedType = this.embeddedType(parent.type, relationship.key);
+ if (embeddedType === 'always') {
+ reference.parent = parent;
+ }
+
+ // If the embedded children have an inverse belongs-to, set the
+ // inverse to the current record in their prematerialized data.
+ var parentType = relationship.parentType,
+ inverse = parentType.inverseFor(relationship.key);
+
+ if (inverse) {
+ var inverseName = inverse.name;
+ reference.prematerialized[inverseName] = parent;
+ }
+
+ return reference;
+ }, this);
+
+ prematerialized[relationship.key] = references;
+ },
+
+ extractEmbeddedBelongsTo: function(loader, relationship, data, parent, prematerialized) {
+ var foundType = this.extractEmbeddedType(relationship, data),
+ reference = this.extractRecordRepresentation(loader, foundType, data, true);
+ prematerialized[relationship.key] = reference;
+
+ // If the embedded record should also be saved back when serializing the parent,
+ // make sure we set its parent since it will not have an ID.
+ var embeddedType = this.embeddedType(parent.type, relationship.key);
+ if (embeddedType === 'always') {
+ reference.parent = parent;
+ }
+ },
+
+ /**
+ A hook you can use to customize how the record's type is extracted from
+ the serialized data.
+
+ The `extractEmbeddedType` hook is called with:
+
+ * the relationship
+ * the serialized representation of the record
+
+ By default, it returns the type of the relationship.
+
+ @method extractEmbeddedType
+ @param {Object} relationship an object representing the relationship
+ @param {any} data the serialized representation of the record
+ */
+ extractEmbeddedType: function(relationship, data) {
+ return relationship.type;
+ },
+
+ /**
+ A hook you need to implement in order to extract
+ the data associated with an embedded record.
+
+ @param {any} data the serialized representation of the record
+ @param {String} key the key that represents the embedded record
+ */
+ extractEmbeddedData: mustImplement(),
+
+ //.......................
+ //. SERIALIZATION HOOKS
+ //.......................
+
+ /**
+ The main entry point for serializing a record. While you can consider this
+ a hook that can be overridden in your serializer, you will have to manually
+ handle serialization. For most cases, there are more granular hooks that you
+ can override.
+
+ If overriding this method, these are the responsibilities that you will need
+ to implement yourself:
+
+ * If the option hash contains `includeId`, add the record's ID to the serialized form.
+ By default, `serialize` calls `addId` if appropriate.
+ * If the option hash contains `includeType`, add the record's type to the serialized form.
+ * Add the record's attributes to the serialized form. By default, `serialize` calls
+ `addAttributes`.
+ * Add the record's relationships to the serialized form. By default, `serialize` calls
+ `addRelationships`.
+
+ @method serialize
+ @param {DS.Model} record the record to serialize
+ @param {Object} [options] a hash of options
+ @returns {any} the serialized form of the record
+ */
+ serialize: function(record, options) {
+ options = options || {};
+
+ var serialized = this.createSerializedForm(), id;
+
+ if (options.includeId) {
+ if (id = get(record, 'id')) {
+ this._addId(serialized, record.constructor, id);
+ }
+ }
+
+ if (options.includeType) {
+ this.addType(serialized, record.constructor);
+ }
+
+ this.addAttributes(serialized, record);
+ this.addRelationships(serialized, record);
+
+ return serialized;
+ },
+
+ /**
+ @private
+
+ Given an attribute type and value, convert the value into the
+ serialized form using the transform registered for that type.
+
+ @method serializeValue
+ @param {any} value the value to convert to the serialized form
+ @param {String} attributeType the registered type (e.g. `string`
+ or `boolean`)
+ @returns {any} the serialized form of the value
+ */
+ serializeValue: function(value, attributeType) {
+ var transform = this.transforms ? this.transforms[attributeType] : null;
+
+ Ember.assert("You tried to use an attribute type (" + attributeType + ") that has not been registered", transform);
+ return transform.serialize(value);
+ },
+
+ /**
+ A hook you can use to normalize IDs before adding them to the
+ serialized representation.
+
+ Because the store coerces all IDs to strings for consistency,
+ this is the opportunity for the serializer to, for example,
+ convert numerical IDs back into number form.
+
+ @param {String} id the id from the record
+ @returns {any} the serialized representation of the id
+ */
+ serializeId: function(id) {
+ if (isNaN(id)) { return id; }
+ return +id;
+ },
+
+ /**
+ A hook you can use to change how attributes are added to the serialized
+ representation of a record.
+
+ By default, `addAttributes` simply loops over all of the attributes of the
+ passed record, maps the attribute name to the key for the serialized form,
+ and invokes any registered transforms on the value. It then invokes the
+ more granular `addAttribute` with the key and transformed value.
+
+ Since you can override `keyForAttributeName`, `addAttribute`, and register
+ custom transforms, you should rarely need to override this hook.
+
+ @method addAttributes
+ @param {any} data the serialized representation that is being built
+ @param {DS.Model} record the record to serialize
+ */
+ addAttributes: function(data, record) {
+ record.eachAttribute(function(name, attribute) {
+ this._addAttribute(data, record, name, attribute.type);
+ }, this);
+ },
+
+ /**
+ A hook you can use to customize how the key/value pair is added to
+ the serialized data.
+
+ @method addAttribute
+ @param {any} serialized the serialized form being built
+ @param {String} key the key to add to the serialized data
+ @param {any} value the value to add to the serialized data
+ */
+ addAttribute: mustImplement('addAttribute'),
+
+ /**
+ A hook you can use to customize how the record's id is added to
+ the serialized data.
+
+ The `addId` hook is called with:
+
+ * the serialized representation being built
+ * the resolved primary key (taking configurations and the
+ `primaryKey` hook into consideration)
+ * the serialized id (after calling the `serializeId` hook)
+
+ @method addId
+ @param {any} data the serialized representation that is being built
+ @param {String} key the resolved primary key
+ @param {id} id the serialized id
+ */
+ addId: mustImplement('addId'),
+
+ /**
+ A hook you can use to customize how the record's type is added to
+ the serialized data.
+
+ The `addType` hook is called with:
+
+ * the serialized representation being built
+ * the serialized id (after calling the `serializeId` hook)
+
+ @method addType
+ @param {any} data the serialized representation that is being built
+ @param {DS.Model subclass} type the type of the record
+ */
+ addType: Ember.K,
+
+ /**
+ Creates an empty hash that will be filled in by the hooks called from the
+ `serialize()` method.
+
+ @method createSerializedForm
+ @return {Object}
+ */
+ createSerializedForm: function() {
+ return {};
+ },
+
+ /**
+ A hook you can use to change how relationships are added to the serialized
+ representation of a record.
+
+ By default, `addRelationships` loops over all of the relationships of the
+ passed record, maps the relationship names to the key for the serialized form,
+ and then invokes the public `addBelongsTo` and `addHasMany` hooks.
+
+ Since you can override `keyForBelongsTo`, `keyForHasMany`, `addBelongsTo`,
+ `addHasMany`, and register mappings, you should rarely need to override this
+ hook.
+
+ @method addRelationships
+ @param {any} data the serialized representation that is being built
+ @param {DS.Model} record the record to serialize
+ */
+ addRelationships: function(data, record) {
+ record.eachRelationship(function(name, relationship) {
+ if (relationship.kind === 'belongsTo') {
+ this._addBelongsTo(data, record, name, relationship);
+ } else if (relationship.kind === 'hasMany') {
+ this._addHasMany(data, record, name, relationship);
+ }
+ }, this);
+ },
+
+ /**
+ A hook you can use to add a `belongsTo` relationship to the
+ serialized representation.
+
+ The specifics of this hook are very adapter-specific, so there
+ is no default implementation. You can see `DS.JSONSerializer`
+ for an example of an implementation of the `addBelongsTo` hook.
+
+ The `belongsTo` relationship object has the following properties:
+
+ * **type** a subclass of DS.Model that is the type of the
+ relationship. This is the first parameter to DS.belongsTo
+ * **options** the options passed to the call to DS.belongsTo
+ * **kind** always `belongsTo`
+
+ Additional properties may be added in the future.
+
+ @method addBelongsTo
+ @param {any} data the serialized representation that is being built
+ @param {DS.Model} record the record to serialize
+ @param {String} key the key for the serialized object
+ @param {Object} relationship an object representing the relationship
+ */
+ addBelongsTo: mustImplement('addBelongsTo'),
+
+ /**
+ A hook you can use to add a `hasMany` relationship to the
+ serialized representation.
+
+ The specifics of this hook are very adapter-specific, so there
+ is no default implementation. You may not need to implement this,
+ for example, if your backend only expects relationships on the
+ child of a one to many relationship.
+
+ The `hasMany` relationship object has the following properties:
+
+ * **type** a subclass of DS.Model that is the type of the
+ relationship. This is the first parameter to DS.hasMany
+ * **options** the options passed to the call to DS.hasMany
+ * **kind** always `hasMany`
+
+ Additional properties may be added in the future.
+
+ @method addHasMany
+ @param {any} data the serialized representation that is being built
+ @param {DS.Model} record the record to serialize
+ @param {String} key the key for the serialized object
+ @param {Object} relationship an object representing the relationship
+ */
+ addHasMany: mustImplement('addHasMany'),
+
+ /**
+ NAMING CONVENTIONS
+
+ The most commonly overridden APIs of the serializer are
+ the naming convention methods:
+
+ * `keyForAttributeName`: converts a camelized attribute name
+ into a key in the adapter-provided data hash. For example,
+ if the model's attribute name was `firstName`, and the
+ server used underscored names, you would return `first_name`.
+ * `primaryKey`: returns the key that should be used to
+ extract the id from the adapter-provided data hash. It is
+ also used when serializing a record.
+ */
+
+ /**
+ A hook you can use in your serializer subclass to customize
+ how an unmapped attribute name is converted into a key.
+
+ By default, this method returns the `name` parameter.
+
+ For example, if the attribute names in your JSON are underscored,
+ you will want to convert them into JavaScript conventional
+ camelcase:
+
+ ```javascript
+ App.MySerializer = DS.Serializer.extend({
+ // ...
+
+ keyForAttributeName: function(type, name) {
+ return name.camelize();
+ }
+ });
+ ```
+
+ @method keyForAttributeName
+ @param {DS.Model subclass} type the type of the record with
+ the attribute name `name`
+ @param {String} name the attribute name to convert into a key
+
+ @returns {String} the key
+ */
+ keyForAttributeName: function(type, name) {
+ return name;
+ },
+
+ /**
+ A hook you can use in your serializer to specify a conventional
+ primary key.
+
+ By default, this method will return the string `id`.
+
+ In general, you should not override this hook to specify a special
+ primary key for an individual type; use `configure` instead.
+
+ For example, if your primary key is always `__id__`:
+
+ ```javascript
+ App.MySerializer = DS.Serializer.extend({
+ // ...
+ primaryKey: function(type) {
+ return '__id__';
+ }
+ });
+ ```
+
+ In another example, if the primary key always includes the
+ underscored version of the type before the string `id`:
+
+ ```javascript
+ App.MySerializer = DS.Serializer.extend({
+ // ...
+ primaryKey: function(type) {
+ // If the type is `BlogPost`, this will return
+ // `blog_post_id`.
+ var typeString = type.toString().split(".")[1].underscore();
+ return typeString + "_id";
+ }
+ });
+ ```
+
+ @method primaryKey
+ @param {DS.Model subclass} type
+ @returns {String} the primary key for the type
+ */
+ primaryKey: function(type) {
+ return "id";
+ },
+
+ /**
+ A hook you can use in your serializer subclass to customize
+ how an unmapped `belongsTo` relationship is converted into
+ a key.
+
+ By default, this method calls `keyForAttributeName`, so if
+ your naming convention is uniform across attributes and
+ relationships, you can use the default here and override
+ just `keyForAttributeName` as needed.
+
+ For example, if the `belongsTo` names in your JSON always
+ begin with `BT_` (e.g. `BT_posts`), you can strip out the
+ `BT_` prefix:"
+
+ ```javascript
+ App.MySerializer = DS.Serializer.extend({
+ // ...
+ keyForBelongsTo: function(type, name) {
+ return name.match(/^BT_(.*)$/)[1].camelize();
+ }
+ });
+ ```
+
+ @method keyForBelongsTo
+ @param {DS.Model subclass} type the type of the record with
+ the `belongsTo` relationship.
+ @param {String} name the relationship name to convert into a key
+
+ @returns {String} the key
+ */
+ keyForBelongsTo: function(type, name) {
+ return this.keyForAttributeName(type, name);
+ },
+
+ /**
+ A hook you can use in your serializer subclass to customize
+ how an unmapped `hasMany` relationship is converted into
+ a key.
+
+ By default, this method calls `keyForAttributeName`, so if
+ your naming convention is uniform across attributes and
+ relationships, you can use the default here and override
+ just `keyForAttributeName` as needed.
+
+ For example, if the `hasMany` names in your JSON always
+ begin with the "table name" for the current type (e.g.
+ `post_comments`), you can strip out the prefix:"
+
+ ```javascript
+ App.MySerializer = DS.Serializer.extend({
+ // ...
+ keyForHasMany: function(type, name) {
+ // if your App.BlogPost has many App.BlogComment, the key from
+ // the server would look like: `blog_post_blog_comments`
+ //
+ // 1. Convert the type into a string and underscore the
+ // second part (App.BlogPost -> blog_post)
+ // 2. Extract the part after `blog_post_` (`blog_comments`)
+ // 3. Underscore it, to become `blogComments`
+ var typeString = type.toString().split(".")[1].underscore();
+ return name.match(new RegExp("^" + typeString + "_(.*)$"))[1].camelize();
+ }
+ });
+ ```
+
+ @method keyForHasMany
+ @param {DS.Model subclass} type the type of the record with
+ the `belongsTo` relationship.
+ @param {String} name the relationship name to convert into a key
+
+ @returns {String} the key
+ */
+ keyForHasMany: function(type, name) {
+ return this.keyForAttributeName(type, name);
+ },
+
+ //.........................
+ //. MATERIALIZATION HOOKS
+ //.........................
+
+ materialize: function(record, serialized, prematerialized) {
+ var id;
+ if (Ember.isNone(get(record, 'id'))) {
+ if (prematerialized && prematerialized.hasOwnProperty('id')) {
+ id = prematerialized.id;
+ } else {
+ id = this.extractId(record.constructor, serialized);
+ }
+ record.materializeId(id);
+ }
+
+ this.materializeAttributes(record, serialized, prematerialized);
+ this.materializeRelationships(record, serialized, prematerialized);
+ },
+
+ deserializeValue: function(value, attributeType) {
+ var transform = this.transforms ? this.transforms[attributeType] : null;
+
+ Ember.assert("You tried to use an attribute type (" + attributeType + ") that has not been registered", transform);
+ return transform.deserialize(value);
+ },
+
+ materializeAttributes: function(record, serialized, prematerialized) {
+ record.eachAttribute(function(name, attribute) {
+ if (prematerialized && prematerialized.hasOwnProperty(name)) {
+ record.materializeAttribute(name, prematerialized[name]);
+ } else {
+ this.materializeAttribute(record, serialized, name, attribute.type);
+ }
+ }, this);
+ },
+
+ materializeAttribute: function(record, serialized, attributeName, attributeType) {
+ var value = this.extractAttribute(record.constructor, serialized, attributeName);
+ value = this.deserializeValue(value, attributeType);
+
+ record.materializeAttribute(attributeName, value);
+ },
+
+ materializeRelationships: function(record, serialized, prematerialized) {
+ record.eachRelationship(function(name, relationship) {
+ if (relationship.kind === 'hasMany') {
+ if (prematerialized && prematerialized.hasOwnProperty(name)) {
+ var tuplesOrReferencesOrOpaque = this._convertPrematerializedHasMany(relationship.type, prematerialized[name]);
+ record.materializeHasMany(name, tuplesOrReferencesOrOpaque);
+ } else {
+ this.materializeHasMany(name, record, serialized, relationship, prematerialized);
+ }
+ } else if (relationship.kind === 'belongsTo') {
+ if (prematerialized && prematerialized.hasOwnProperty(name)) {
+ var tupleOrReference = this._convertTuple(relationship.type, prematerialized[name]);
+ record.materializeBelongsTo(name, tupleOrReference);
+ } else {
+ this.materializeBelongsTo(name, record, serialized, relationship, prematerialized);
+ }
+ }
+ }, this);
+ },
+
+ materializeHasMany: function(name, record, hash, relationship) {
+ var type = record.constructor,
+ key = this._keyForHasMany(type, relationship.key),
+ idsOrTuples = this.extractHasMany(type, hash, key),
+ tuples = idsOrTuples;
+
+ if(idsOrTuples && Ember.isArray(idsOrTuples)) {
+ tuples = this._convertTuples(relationship.type, idsOrTuples);
+ }
+
+ record.materializeHasMany(name, tuples);
+ },
+
+ materializeBelongsTo: function(name, record, hash, relationship) {
+ var type = record.constructor,
+ key = this._keyForBelongsTo(type, relationship.key),
+ idOrTuple,
+ tuple = null;
+
+ if(relationship.options && relationship.options.polymorphic) {
+ idOrTuple = this.extractBelongsToPolymorphic(type, hash, key);
+ } else {
+ idOrTuple = this.extractBelongsTo(type, hash, key);
+ }
+
+ if(!isNone(idOrTuple)) {
+ tuple = this._convertTuple(relationship.type, idOrTuple);
+ }
+
+ record.materializeBelongsTo(name, tuple);
+ },
+
+ _convertPrematerializedHasMany: function(type, prematerializedHasMany) {
+ var tuplesOrReferencesOrOpaque;
+ if( typeof prematerializedHasMany === 'string' ) {
+ tuplesOrReferencesOrOpaque = prematerializedHasMany;
+ } else {
+ tuplesOrReferencesOrOpaque = this._convertTuples(type, prematerializedHasMany);
+ }
+ return tuplesOrReferencesOrOpaque;
+ },
+
+ _convertTuples: function(type, idsOrTuples) {
+ return map.call(idsOrTuples, function(idOrTuple) {
+ return this._convertTuple(type, idOrTuple);
+ }, this);
+ },
+
+ _convertTuple: function(type, idOrTuple) {
+ var foundType;
+
+ if (typeof idOrTuple === 'object') {
+ if (DS.Model.detect(idOrTuple.type)) {
+ return idOrTuple;
+ } else {
+ foundType = this.typeFromAlias(idOrTuple.type);
+ Ember.assert("Unable to resolve type " + idOrTuple.type + ". You may need to configure your serializer aliases.", !!foundType);
+
+ return {id: idOrTuple.id, type: foundType};
+ }
+ }
+ return {id: idOrTuple, type: type};
+ },
+
+ /**
+ @private
+
+ This method is called to get the primary key for a given
+ type.
+
+ If a primary key configuration exists for this type, this
+ method will return the configured value. Otherwise, it will
+ call the public `primaryKey` hook.
+
+ @method _primaryKey
+ @param {DS.Model subclass} type
+ @returns {String} the primary key for the type
+ */
+ _primaryKey: function(type) {
+ var config = this.configurationForType(type),
+ primaryKey = config && config.primaryKey;
+
+ if (primaryKey) {
+ return primaryKey;
+ } else {
+ return this.primaryKey(type);
+ }
+ },
+
+ /**
+ @private
+
+ This method looks up the key for the attribute name and transforms the
+ attribute's value using registered transforms.
+
+ Specifically:
+
+ 1. Look up the key for the attribute name. If available, this will use
+ any registered mappings. Otherwise, it will invoke the public
+ `keyForAttributeName` hook.
+ 2. Get the value from the record using the `attributeName`.
+ 3. Transform the value using registered transforms for the `attributeType`.
+ 4. Invoke the public `addAttribute` hook with the hash, key, and
+ transformed value.
+
+ @method _addAttribute
+ @param {any} data the serialized representation being built
+ @param {DS.Model} record the record to serialize
+ @param {String} attributeName the name of the attribute on the record
+ @param {String} attributeType the type of the attribute (e.g. `string`
+ or `boolean`)
+ */
+ _addAttribute: function(data, record, attributeName, attributeType) {
+ var key = this._keyForAttributeName(record.constructor, attributeName);
+ var value = get(record, attributeName);
+
+ this.addAttribute(data, key, this.serializeValue(value, attributeType));
+ },
+
+ /**
+ @private
+
+ This method looks up the primary key for the `type` and invokes
+ `serializeId` on the `id`.
+
+ It then invokes the public `addId` hook with the primary key and
+ the serialized id.
+
+ @method _addId
+ @param {any} data the serialized representation that is being built
+ @param {Ember.Model subclass} type
+ @param {any} id the materialized id from the record
+ */
+ _addId: function(hash, type, id) {
+ var primaryKey = this._primaryKey(type);
+
+ this.addId(hash, primaryKey, this.serializeId(id));
+ },
+
+ /**
+ @private
+
+ This method is called to get a key used in the data from
+ an attribute name. It first checks for any mappings before
+ calling the public hook `keyForAttributeName`.
+
+ @method _keyForAttributeName
+ @param {DS.Model subclass} type the type of the record with
+ the attribute name `name`
+ @param {String} name the attribute name to convert into a key
+
+ @returns {String} the key
+ */
+ _keyForAttributeName: function(type, name) {
+ return this._keyFromMappingOrHook('keyForAttributeName', type, name);
+ },
+
+ /**
+ @private
+
+ This method is called to get a key used in the data from
+ a belongsTo relationship. It first checks for any mappings before
+ calling the public hook `keyForBelongsTo`.
+
+ @method _keyForBelongsTo
+ @param {DS.Model subclass} type the type of the record with
+ the `belongsTo` relationship.
+ @param {String} name the relationship name to convert into a key
+
+ @returns {String} the key
+ */
+ _keyForBelongsTo: function(type, name) {
+ return this._keyFromMappingOrHook('keyForBelongsTo', type, name);
+ },
+
+ keyFor: function(description) {
+ var type = description.parentType,
+ name = description.key;
+
+ switch (description.kind) {
+ case 'belongsTo':
+ return this._keyForBelongsTo(type, name);
+ case 'hasMany':
+ return this._keyForHasMany(type, name);
+ }
+ },
+
+ /**
+ @private
+
+ This method is called to get a key used in the data from
+ a hasMany relationship. It first checks for any mappings before
+ calling the public hook `keyForHasMany`.
+
+ @method _keyForHasMany
+ @param {DS.Model subclass} type the type of the record with
+ the `hasMany` relationship.
+ @param {String} name the relationship name to convert into a key
+
+ @returns {String} the key
+ */
+ _keyForHasMany: function(type, name) {
+ return this._keyFromMappingOrHook('keyForHasMany', type, name);
+ },
+ /**
+ @private
+
+ This method converts the relationship name to a key for serialization,
+ and then invokes the public `addBelongsTo` hook.
+
+ @method _addBelongsTo
+ @param {any} data the serialized representation that is being built
+ @param {DS.Model} record the record to serialize
+ @param {String} name the relationship name
+ @param {Object} relationship an object representing the relationship
+ */
+ _addBelongsTo: function(data, record, name, relationship) {
+ var key = this._keyForBelongsTo(record.constructor, name);
+ this.addBelongsTo(data, record, key, relationship);
+ },
+
+ /**
+ @private
+
+ This method converts the relationship name to a key for serialization,
+ and then invokes the public `addHasMany` hook.
+
+ @method _addHasMany
+ @param {any} data the serialized representation that is being built
+ @param {DS.Model} record the record to serialize
+ @param {String} name the relationship name
+ @param {Object} relationship an object representing the relationship
+ */
+ _addHasMany: function(data, record, name, relationship) {
+ var key = this._keyForHasMany(record.constructor, name);
+ this.addHasMany(data, record, key, relationship);
+ },
+
+ /**
+ @private
+
+ An internal method that handles checking whether a mapping
+ exists for a particular attribute or relationship name before
+ calling the public hooks.
+
+ If a mapping is found, and the mapping has a key defined,
+ use that instead of invoking the hook.
+
+ @method _keyFromMappingOrHook
+ @param {String} publicMethod the public hook to invoke if
+ a mapping is not found (e.g. `keyForAttributeName`)
+ @param {DS.Model subclass} type the type of the record with
+ the attribute or relationship name.
+ @param {String} name the attribute or relationship name to
+ convert into a key
+ */
+ _keyFromMappingOrHook: function(publicMethod, type, name) {
+ var key = this.mappingOption(type, name, 'key');
+
+ if (key) {
+ return key;
+ } else {
+ return this[publicMethod](type, name);
+ }
+ },
+
+ /**
+ TRANSFORMS
+ */
+
+ registerTransform: function(type, transform) {
+ this.transforms[type] = transform;
+ },
+
+ registerEnumTransform: function(type, objects) {
+ var transform = {
+ deserialize: function(serialized) {
+ return Ember.A(objects).objectAt(serialized);
+ },
+ serialize: function(deserialized) {
+ return Ember.EnumerableUtils.indexOf(objects, deserialized);
+ },
+ values: objects
+ };
+ this.registerTransform(type, transform);
+ },
+
+ /**
+ MAPPING CONVENIENCE
+ */
+
+ map: function(type, mappings) {
+ this.mappings.set(type, mappings);
+ },
+
+ configure: function(type, configuration) {
+ if (type && !configuration) {
+ Ember.merge(this.globalConfigurations, type);
+ return;
+ }
+
+ var config, alias;
+
+ if (configuration.alias) {
+ alias = configuration.alias;
+ this.aliases.set(alias, type);
+ delete configuration.alias;
+ }
+
+ config = Ember.create(this.globalConfigurations);
+ Ember.merge(config, configuration);
+
+ this.configurations.set(type, config);
+ },
+
+ typeFromAlias: function(alias) {
+ this._completeAliases();
+ return this.aliases.get(alias);
+ },
+
+ mappingForType: function(type) {
+ this._reifyMappings();
+ return this.mappings.get(type) || {};
+ },
+
+ configurationForType: function(type) {
+ this._reifyConfigurations();
+ return this.configurations.get(type) || this.globalConfigurations;
+ },
+
+ _completeAliases: function() {
+ this._pluralizeAliases();
+ this._reifyAliases();
+ },
+
+ _pluralizeAliases: function() {
+ if (this._didPluralizeAliases) { return; }
+
+ var aliases = this.aliases,
+ sideloadMapping = this.aliases.sideloadMapping,
+ plural,
+ self = this;
+
+ aliases.forEach(function(key, type) {
+ plural = self.pluralize(key);
+ Ember.assert("The '" + key + "' alias has already been defined", !aliases.get(plural));
+ aliases.set(plural, type);
+ });
+
+ // This map is only for backward compatibility with the `sideloadAs` option.
+ if (sideloadMapping) {
+ sideloadMapping.forEach(function(key, type) {
+ Ember.assert("The '" + key + "' alias has already been defined", !aliases.get(key) || (aliases.get(key)===type) );
+ aliases.set(key, type);
+ });
+ delete this.aliases.sideloadMapping;
+ }
+
+ this._didPluralizeAliases = true;
+ },
+
+ _reifyAliases: function() {
+ if (this._didReifyAliases) { return; }
+
+ var aliases = this.aliases,
+ reifiedAliases = Ember.Map.create(),
+ foundType;
+
+ aliases.forEach(function(key, type) {
+ if (typeof type === 'string') {
+ foundType = Ember.get(Ember.lookup, type);
+ Ember.assert("Could not find model at path " + key, type);
+
+ reifiedAliases.set(key, foundType);
+ } else {
+ reifiedAliases.set(key, type);
+ }
+ });
+
+ this.aliases = reifiedAliases;
+ this._didReifyAliases = true;
+ },
+
+ _reifyMappings: function() {
+ if (this._didReifyMappings) { return; }
+
+ var mappings = this.mappings,
+ reifiedMappings = Ember.Map.create();
+
+ mappings.forEach(function(key, mapping) {
+ if (typeof key === 'string') {
+ var type = Ember.get(Ember.lookup, key);
+ Ember.assert("Could not find model at path " + key, type);
+
+ reifiedMappings.set(type, mapping);
+ } else {
+ reifiedMappings.set(key, mapping);
+ }
+ });
+
+ this.mappings = reifiedMappings;
+
+ this._didReifyMappings = true;
+ },
+
+ _reifyConfigurations: function() {
+ if (this._didReifyConfigurations) { return; }
+
+ var configurations = this.configurations,
+ reifiedConfigurations = Ember.Map.create();
+
+ configurations.forEach(function(key, mapping) {
+ if (typeof key === 'string' && key !== 'plurals') {
+ var type = Ember.get(Ember.lookup, key);
+ Ember.assert("Could not find model at path " + key, type);
+
+ reifiedConfigurations.set(type, mapping);
+ } else {
+ reifiedConfigurations.set(key, mapping);
+ }
+ });
+
+ this.configurations = reifiedConfigurations;
+
+ this._didReifyConfigurations = true;
+ },
+
+ mappingOption: function(type, name, option) {
+ var mapping = this.mappingForType(type)[name];
+
+ return mapping && mapping[option];
+ },
+
+ configOption: function(type, option) {
+ var config = this.configurationForType(type);
+
+ return config[option];
+ },
+
+ // EMBEDDED HELPERS
+
+ embeddedType: function(type, name) {
+ return this.mappingOption(type, name, 'embedded');
+ },
+
+ eachEmbeddedRecord: function(record, callback, binding) {
+ this.eachEmbeddedBelongsToRecord(record, callback, binding);
+ this.eachEmbeddedHasManyRecord(record, callback, binding);
+ },
+
+ eachEmbeddedBelongsToRecord: function(record, callback, binding) {
+ this.eachEmbeddedBelongsTo(record.constructor, function(name, relationship, embeddedType) {
+ var embeddedRecord = get(record, name);
+ if (embeddedRecord) { callback.call(binding, embeddedRecord, embeddedType); }
+ });
+ },
+
+ eachEmbeddedHasManyRecord: function(record, callback, binding) {
+ this.eachEmbeddedHasMany(record.constructor, function(name, relationship, embeddedType) {
+ var array = get(record, name);
+ for (var i=0, l=get(array, 'length'); i<l; i++) {
+ callback.call(binding, array.objectAt(i), embeddedType);
+ }
+ });
+ },
+
+ eachEmbeddedHasMany: function(type, callback, binding) {
+ this.eachEmbeddedRelationship(type, 'hasMany', callback, binding);
+ },
+
+ eachEmbeddedBelongsTo: function(type, callback, binding) {
+ this.eachEmbeddedRelationship(type, 'belongsTo', callback, binding);
+ },
+
+ eachEmbeddedRelationship: function(type, kind, callback, binding) {
+ type.eachRelationship(function(name, relationship) {
+ var embeddedType = this.embeddedType(type, name);
+
+ if (embeddedType) {
+ if (relationship.kind === kind) {
+ callback.call(binding, name, relationship, embeddedType);
+ }
+ }
+ }, this);
+ },
+
+ // HELPERS
+
+ // define a plurals hash in your subclass to define
+ // special-case pluralization
+ pluralize: function(name) {
+ var plurals = this.configurations.get('plurals');
+ return (plurals && plurals[name]) || name + "s";
+ },
+
+ // use the same plurals hash to determine
+ // special-case singularization
+ singularize: function(name) {
+ var plurals = this.configurations.get('plurals');
+ if (plurals) {
+ for (var i in plurals) {
+ if (plurals[i] === name) {
+ return i;
+ }
+ }
+ }
+ if (name.lastIndexOf('s') === name.length - 1) {
+ return name.substring(0, name.length - 1);
+ } else {
+ return name;
+ }
+ }
+});
+
+
+})();
+
+
+
+(function() {
+var isNone = Ember.isNone, isEmpty = Ember.isEmpty;
+
+/**
+ @module data
+ @submodule data-transforms
+*/
+
+/**
+ DS.Transforms is a hash of transforms used by DS.Serializer.
+
+ @class JSONTransforms
+ @static
+ @namespace DS
+*/
+DS.JSONTransforms = {
+ string: {
+ deserialize: function(serialized) {
+ return isNone(serialized) ? null : String(serialized);
+ },
+
+ serialize: function(deserialized) {
+ return isNone(deserialized) ? null : String(deserialized);
+ }
+ },
+
+ number: {
+ deserialize: function(serialized) {
+ return isEmpty(serialized) ? null : Number(serialized);
+ },
+
+ serialize: function(deserialized) {
+ return isEmpty(deserialized) ? null : Number(deserialized);
+ }
+ },
+
+ // Handles the following boolean inputs:
+ // "TrUe", "t", "f", "FALSE", 0, (non-zero), or boolean true/false
+ 'boolean': {
+ deserialize: function(serialized) {
+ var type = typeof serialized;
+
+ if (type === "boolean") {
+ return serialized;
+ } else if (type === "string") {
+ return serialized.match(/^true$|^t$|^1$/i) !== null;
+ } else if (type === "number") {
+ return serialized === 1;
+ } else {
+ return false;
+ }
+ },
+
+ serialize: function(deserialized) {
+ return Boolean(deserialized);
+ }
+ },
+
+ date: {
+ deserialize: function(serialized) {
+ var type = typeof serialized;
+
+ if (type === "string") {
+ return new Date(Ember.Date.parse(serialized));
+ } else if (type === "number") {
+ return new Date(serialized);
+ } else if (serialized === null || serialized === undefined) {
+ // if the value is not present in the data,
+ // return undefined, not null.
+ return serialized;
+ } else {
+ return null;
+ }
+ },
+
+ serialize: function(date) {
+ if (date instanceof Date) {
+ var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
+ var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
+
+ var pad = function(num) {
+ return num < 10 ? "0"+num : ""+num;
+ };
+
+ var utcYear = date.getUTCFullYear(),
+ utcMonth = date.getUTCMonth(),
+ utcDayOfMonth = date.getUTCDate(),
+ utcDay = date.getUTCDay(),
+ utcHours = date.getUTCHours(),
+ utcMinutes = date.getUTCMinutes(),
+ utcSeconds = date.getUTCSeconds();
+
+
+ var dayOfWeek = days[utcDay];
+ var dayOfMonth = pad(utcDayOfMonth);
+ var month = months[utcMonth];
+
+ return dayOfWeek + ", " + dayOfMonth + " " + month + " " + utcYear + " " +
+ pad(utcHours) + ":" + pad(utcMinutes) + ":" + pad(utcSeconds) + " GMT";
+ } else {
+ return null;
+ }
+ }
+ }
+};
+
+})();
+
+
+
+(function() {
+/**
+ @module data
+ @submodule data-serializers
+*/
+
+/**
+ @class JSONSerializer
+ @constructor
+ @namespace DS
+ @extends DS.Serializer
+*/
+
+var get = Ember.get, set = Ember.set;
+
+DS.JSONSerializer = DS.Serializer.extend({
+ init: function() {
+ this._super();
+
+ if (!get(this, 'transforms')) {
+ this.set('transforms', DS.JSONTransforms);
+ }
+
+ this.sideloadMapping = Ember.Map.create();
+ this.metadataMapping = Ember.Map.create();
+
+ this.configure({
+ meta: 'meta',
+ since: 'since'
+ });
+ },
+
+ configure: function(type, configuration) {
+ var key;
+
+ if (type && !configuration) {
+ for(key in type){
+ this.metadataMapping.set(get(type, key), key);
+ }
+
+ return this._super(type);
+ }
+
+ var sideloadAs = configuration.sideloadAs,
+ sideloadMapping;
+
+ if (sideloadAs) {
+ sideloadMapping = this.aliases.sideloadMapping || Ember.Map.create();
+ sideloadMapping.set(sideloadAs, type);
+ this.aliases.sideloadMapping = sideloadMapping;
+ delete configuration.sideloadAs;
+ }
+
+ this._super.apply(this, arguments);
+ },
+
+ addId: function(data, key, id) {
+ data[key] = id;
+ },
+
+ /**
+ A hook you can use to customize how the key/value pair is added to
+ the serialized data.
+
+ @param {any} hash the JSON hash being built
+ @param {String} key the key to add to the serialized data
+ @param {any} value the value to add to the serialized data
+ */
+ addAttribute: function(hash, key, value) {
+ hash[key] = value;
+ },
+
+ extractAttribute: function(type, hash, attributeName) {
+ var key = this._keyForAttributeName(type, attributeName);
+ return hash[key];
+ },
+
+ extractId: function(type, hash) {
+ var primaryKey = this._primaryKey(type);
+
+ if (hash.hasOwnProperty(primaryKey)) {
+ // Ensure that we coerce IDs to strings so that record
+ // IDs remain consistent between application runs; especially
+ // if the ID is serialized and later deserialized from the URL,
+ // when type information will have been lost.
+ return hash[primaryKey]+'';
+ } else {
+ return null;
+ }
+ },
+
+ extractEmbeddedData: function(hash, key) {
+ return hash[key];
+ },
+
+ extractHasMany: function(type, hash, key) {
+ return hash[key];
+ },
+
+ extractBelongsTo: function(type, hash, key) {
+ return hash[key];
+ },
+
+ extractBelongsToPolymorphic: function(type, hash, key) {
+ var keyForId = this.keyForPolymorphicId(key),
+ keyForType,
+ id = hash[keyForId];
+
+ if (id) {
+ keyForType = this.keyForPolymorphicType(key);
+ return {id: id, type: hash[keyForType]};
+ }
+
+ return null;
+ },
+
+ addBelongsTo: function(hash, record, key, relationship) {
+ var type = record.constructor,
+ name = relationship.key,
+ value = null,
+ includeType = (relationship.options && relationship.options.polymorphic),
+ embeddedChild,
+ child,
+ id;
+
+ if (this.embeddedType(type, name)) {
+ if (embeddedChild = get(record, name)) {
+ value = this.serialize(embeddedChild, { includeId: true, includeType: includeType });
+ }
+
+ hash[key] = value;
+ } else {
+ child = get(record, relationship.key);
+ id = get(child, 'id');
+
+ if (relationship.options && relationship.options.polymorphic && !Ember.isNone(id)) {
+ this.addBelongsToPolymorphic(hash, key, id, child.constructor);
+ } else {
+ hash[key] = id === undefined ? null : this.serializeId(id);
+ }
+ }
+ },
+
+ addBelongsToPolymorphic: function(hash, key, id, type) {
+ var keyForId = this.keyForPolymorphicId(key),
+ keyForType = this.keyForPolymorphicType(key);
+ hash[keyForId] = id;
+ hash[keyForType] = this.rootForType(type);
+ },
+
+ /**
+ Adds a has-many relationship to the JSON hash being built.
+
+ The default REST semantics are to only add a has-many relationship if it
+ is embedded. If the relationship was initially loaded by ID, we assume that
+ that was done as a performance optimization, and that changes to the
+ has-many should be saved as foreign key changes on the child's belongs-to
+ relationship.
+
+ @param {Object} hash the JSON being built
+ @param {DS.Model} record the record being serialized
+ @param {String} key the JSON key into which the serialized relationship
+ should be saved
+ @param {Object} relationship metadata about the relationship being serialized
+ */
+
+ addHasMany: function(hash, record, key, relationship) {
+ var type = record.constructor,
+ name = relationship.key,
+ serializedHasMany = [],
+ includeType = (relationship.options && relationship.options.polymorphic),
+ manyArray, embeddedType;
+
+ // If the has-many is not embedded, there is nothing to do.
+ embeddedType = this.embeddedType(type, name);
+ if (embeddedType !== 'always') { return; }
+
+ // Get the DS.ManyArray for the relationship off the record
+ manyArray = get(record, name);
+
+ // Build up the array of serialized records
+ manyArray.forEach(function (record) {
+ serializedHasMany.push(this.serialize(record, { includeId: true, includeType: includeType }));
+ }, this);
+
+ // Set the appropriate property of the serialized JSON to the
+ // array of serialized embedded records
+ hash[key] = serializedHasMany;
+ },
+
+ addType: function(hash, type) {
+ var keyForType = this.keyForEmbeddedType();
+ hash[keyForType] = this.rootForType(type);
+ },
+
+ // EXTRACTION
+
+ extract: function(loader, json, type, record) {
+ var root = this.rootForType(type);
+
+ this.sideload(loader, type, json, root);
+ this.extractMeta(loader, type, json);
+
+ if (json[root]) {
+ if (record) { loader.updateId(record, json[root]); }
+ this.extractRecordRepresentation(loader, type, json[root]);
+ } else {
+ Ember.Logger.warn("Extract requested, but no data given for " + type + ". This may cause weird problems.");
+ }
+ },
+
+ extractMany: function(loader, json, type, records) {
+ var root = this.rootForType(type);
+ root = this.pluralize(root);
+
+ this.sideload(loader, type, json, root);
+ this.extractMeta(loader, type, json);
+
+ if (json[root]) {
+ var objects = json[root], references = [];
+ if (records) { records = records.toArray(); }
+
+ for (var i = 0; i < objects.length; i++) {
+ if (records) { loader.updateId(records[i], objects[i]); }
+ var reference = this.extractRecordRepresentation(loader, type, objects[i]);
+ references.push(reference);
+ }
+
+ loader.populateArray(references);
+ }
+ },
+
+ extractMeta: function(loader, type, json) {
+ var meta = this.configOption(type, 'meta'),
+ data = json, value;
+
+ if(meta && json[meta]){
+ data = json[meta];
+ }
+
+ this.metadataMapping.forEach(function(property, key){
+ value = data[property];
+ if(!Ember.isNone(value)){
+ loader.metaForType(type, key, value);
+ }
+ });
+ },
+
+ extractEmbeddedType: function(relationship, data) {
+ var foundType = relationship.type;
+ if(relationship.options && relationship.options.polymorphic) {
+ var key = this.keyFor(relationship),
+ keyForEmbeddedType = this.keyForEmbeddedType(key);
+
+ foundType = this.typeFromAlias(data[keyForEmbeddedType]);
+ delete data[keyForEmbeddedType];
+ }
+
+ return foundType;
+ },
+
+ /**
+ @private
+
+ Iterates over the `json` payload and attempts to load any data
+ included alongside `root`.
+
+ The keys expected for sideloaded data are based upon the types related
+ to the root model. Recursion is used to ensure that types related to
+ related types can be loaded as well. Any custom keys specified by
+ `sideloadAs` mappings will also be respected.
+
+ @param {DS.Store subclass} loader
+ @param {DS.Model subclass} type
+ @param {Object} json
+ @param {String} root
+ */
+ sideload: function(loader, type, json, root) {
+ var sideloadedType;
+
+ this.configureSideloadMappingForType(type);
+
+ for (var prop in json) {
+ if (!json.hasOwnProperty(prop) ||
+ prop === root ||
+ !!this.metadataMapping.get(prop)) {
+ continue;
+ }
+
+ sideloadedType = this.typeFromAlias(prop);
+ Ember.assert("Your server returned a hash with the key " + prop + " but you have no mapping for it", !!sideloadedType);
+
+ this.loadValue(loader, sideloadedType, json[prop]);
+ }
+ },
+
+ /**
+ @private
+
+ Configures possible sideload mappings for the types related to a
+ particular model. This recursive method ensures that sideloading
+ works for related models as well.
+
+ @param {DS.Model subclass} type
+ @param {Ember.A} configured an array of types that have already been configured
+ */
+ configureSideloadMappingForType: function(type, configured) {
+ if (!configured) {configured = Ember.A([]);}
+ configured.pushObject(type);
+
+ type.eachRelatedType(function(relatedType) {
+ if (!configured.contains(relatedType)) {
+ var root = this.defaultSideloadRootForType(relatedType);
+ this.aliases.set(root, relatedType);
+
+ this.configureSideloadMappingForType(relatedType, configured);
+ }
+ }, this);
+ },
+
+ loadValue: function(loader, type, value) {
+ if (value instanceof Array) {
+ for (var i=0; i < value.length; i++) {
+ loader.sideload(type, value[i]);
+ }
+ } else {
+ loader.sideload(type, value);
+ }
+ },
+
+ /**
+ A hook you can use in your serializer subclass to customize
+ how a polymorphic association's name is converted into a key for the id.
+
+ @param {String} name the association name to convert into a key
+
+ @return {String} the key
+ */
+ keyForPolymorphicId: function(key){
+ return key;
+ },
+
+ /**
+ A hook you can use in your serializer subclass to customize
+ how a polymorphic association's name is converted into a key for the type.
+
+ @param {String} name the association name to convert into a key
+
+ @return {String} the key
+ */
+ keyForPolymorphicType: function(key){
+ return this.keyForPolymorphicId(key) + '_type';
+ },
+
+ /**
+ A hook you can use in your serializer subclass to customize
+ the key used to store the type of a record of an embedded polymorphic association.
+
+ By default, this method return 'type'.
+
+ @return {String} the key
+ */
+ keyForEmbeddedType: function() {
+ return 'type';
+ },
+
+ // HELPERS
+
+ /**
+ @private
+
+ Determines the singular root name for a particular type.
+
+ This is an underscored, lowercase version of the model name.
+ For example, the type `App.UserGroup` will have the root
+ `user_group`.
+
+ @param {DS.Model subclass} type
+ @return {String} name of the root element
+ */
+ rootForType: function(type) {
+ var typeString = type.toString();
+
+ Ember.assert("Your model must not be anonymous. It was " + type, typeString.charAt(0) !== '(');
+
+ // use the last part of the name as the URL
+ var parts = typeString.split(".");
+ var name = parts[parts.length - 1];
+ return name.replace(/([A-Z])/g, '_$1').toLowerCase().slice(1);
+ },
+
+ /**
+ @private
+
+ The default root name for a particular sideloaded type.
+
+ @param {DS.Model subclass} type
+ @return {String} name of the root element
+ */
+ defaultSideloadRootForType: function(type) {
+ return this.pluralize(this.rootForType(type));
+ }
+});
+
+})();
+
+
+
+(function() {
+/**
+ @module data
+ @submodule data-adapter
+*/
+
+var get = Ember.get, set = Ember.set, merge = Ember.merge;
+
+function loaderFor(store) {
+ return {
+ load: function(type, data, prematerialized) {
+ return store.load(type, data, prematerialized);
+ },
+
+ loadMany: function(type, array) {
+ return store.loadMany(type, array);
+ },
+
+ updateId: function(record, data) {
+ return store.updateId(record, data);
+ },
+
+ populateArray: Ember.K,
+
+ sideload: function(type, data) {
+ return store.adapterForType(type).load(store, type, data);
+ },
+
+ sideloadMany: function(type, array) {
+ return store.loadMany(type, array);
+ },
+
+ prematerialize: function(reference, prematerialized) {
+ reference.prematerialized = prematerialized;
+ },
+
+ metaForType: function(type, property, data) {
+ store.metaForType(type, property, data);
+ }
+ };
+}
+
+DS.loaderFor = loaderFor;
+
+/**
+ An adapter is an object that receives requests from a store and
+ translates them into the appropriate action to take against your
+ persistence layer. The persistence layer is usually an HTTP API, but may
+ be anything, such as the browser's local storage.
+
+ ### Creating an Adapter
+
+ First, create a new subclass of `DS.Adapter`:
+
+ App.MyAdapter = DS.Adapter.extend({
+ // ...your code here
+ });
+
+ To tell your store which adapter to use, set its `adapter` property:
+
+ App.store = DS.Store.create({
+ adapter: App.MyAdapter.create()
+ });
+
+ `DS.Adapter` is an abstract base class that you should override in your
+ application to customize it for your backend. The minimum set of methods
+ that you should implement is:
+
+ * `find()`
+ * `createRecord()`
+ * `updateRecord()`
+ * `deleteRecord()`
+
+ To improve the network performance of your application, you can optimize
+ your adapter by overriding these lower-level methods:
+
+ * `findMany()`
+ * `createRecords()`
+ * `updateRecords()`
+ * `deleteRecords()`
+ * `commit()`
+
+ For an example implementation, see {{#crossLink "DS.RestAdapter"}} the
+ included REST adapter.{{/crossLink}}.
+
+ @class Adapter
+ @namespace DS
+ @extends Ember.Object
+*/
+
+DS.Adapter = Ember.Object.extend(DS._Mappable, {
+
+ init: function() {
+ var serializer = get(this, 'serializer');
+
+ if (Ember.Object.detect(serializer)) {
+ serializer = serializer.create();
+ set(this, 'serializer', serializer);
+ }
+
+ this._attributesMap = this.createInstanceMapFor('attributes');
+ this._configurationsMap = this.createInstanceMapFor('configurations');
+
+ this._outstandingOperations = new Ember.MapWithDefault({
+ defaultValue: function() { return 0; }
+ });
+
+ this._dependencies = new Ember.MapWithDefault({
+ defaultValue: function() { return new Ember.OrderedSet(); }
+ });
+
+ this.registerSerializerTransforms(this.constructor, serializer, {});
+ this.registerSerializerMappings(serializer);
+ },
+
+ /**
+ Loads a payload for a record into the store.
+
+ This method asks the serializer to break the payload into
+ constituent parts, and then loads them into the store. For example,
+ if you have a payload that contains embedded records, they will be
+ extracted by the serializer and loaded into the store.
+
+ For example:
+
+ adapter.load(store, App.Person, {
+ id: 123,
+ firstName: "Yehuda",
+ lastName: "Katz",
+ occupations: [{
+ id: 345,
+ title: "Tricycle Mechanic"
+ }]
+ });
+
+ This will load the payload for the `App.Person` with ID `123` and
+ the embedded `App.Occupation` with ID `345`.
+
+ @method load
+ @param {DS.Store} store
+ @param {subclass of DS.Model} type
+ @param {any} payload
+ */
+ load: function(store, type, payload) {
+ var loader = loaderFor(store);
+ return get(this, 'serializer').extractRecordRepresentation(loader, type, payload);
+ },
+
+ /**
+ Acknowledges that the adapter has finished creating a record.
+
+ Your adapter should call this method from `createRecord` when
+ it has saved a new record to its persistent storage and received
+ an acknowledgement.
+
+ If the persistent storage returns a new payload in response to the
+ creation, and you want to update the existing record with the
+ new information, pass the payload as the fourth parameter.
+
+ For example, the `RESTAdapter` saves newly created records by
+ making an Ajax request. When the server returns, the adapter
+ calls didCreateRecord. If the server returns a response body,
+ it is passed as the payload.
+
+ @method didCreateRecord
+ @param {DS.Store} store
+ @param {subclass of DS.Model} type
+ @param {DS.Model} record
+ @param {any} payload
+ */
+ didCreateRecord: function(store, type, record, payload) {
+ store.didSaveRecord(record);
+
+ if (payload) {
+ var loader = DS.loaderFor(store);
+
+ loader.load = function(type, data, prematerialized) {
+ store.updateId(record, data);
+ return store.load(type, data, prematerialized);
+ };
+
+ get(this, 'serializer').extract(loader, payload, type);
+ }
+ },
+
+ /**
+ Acknowledges that the adapter has finished creating several records.
+
+ Your adapter should call this method from `createRecords` when it
+ has saved multiple created records to its persistent storage
+ received an acknowledgement.
+
+ If the persistent storage returns a new payload in response to the
+ creation, and you want to update the existing record with the
+ new information, pass the payload as the fourth parameter.
+
+ @method didCreateRecords
+ @param {DS.Store} store
+ @param {subclass of DS.Model} type
+ @param {DS.Model} record
+ @param {any} payload
+ */
+ didCreateRecords: function(store, type, records, payload) {
+ records.forEach(function(record) {
+ store.didSaveRecord(record);
+ }, this);
+
+ if (payload) {
+ var loader = DS.loaderFor(store);
+ get(this, 'serializer').extractMany(loader, payload, type, records);
+ }
+ },
+
+ /**
+ @private
+
+ Acknowledges that the adapter has finished updating or deleting a record.
+
+ Your adapter should call this method from `updateRecord` or `deleteRecord`
+ when it has updated or deleted a record to its persistent storage and
+ received an acknowledgement.
+
+ If the persistent storage returns a new payload in response to the
+ update or delete, and you want to update the existing record with the
+ new information, pass the payload as the fourth parameter.
+
+ @method didSaveRecord
+ @param {DS.Store} store
+ @param {subclass of DS.Model} type
+ @param {DS.Model} record
+ @param {any} payload
+ */
+ didSaveRecord: function(store, type, record, payload) {
+ store.didSaveRecord(record);
+
+ var serializer = get(this, 'serializer');
+
+ serializer.eachEmbeddedRecord(record, function(embeddedRecord, embeddedType) {
+ if (embeddedType === 'load') { return; }
+
+ this.didSaveRecord(store, embeddedRecord.constructor, embeddedRecord);
+ }, this);
+
+ if (payload) {
+ var loader = DS.loaderFor(store);
+ serializer.extract(loader, payload, type);
+ }
+ },
+
+ /**
+ Acknowledges that the adapter has finished updating a record.
+
+ Your adapter should call this method from `updateRecord` when it
+ has updated a record to its persistent storage and received an
+ acknowledgement.
+
+ If the persistent storage returns a new payload in response to the
+ update, pass the payload as the fourth parameter.
+
+ @method didUpdateRecord
+ @param {DS.Store} store
+ @param {subclass of DS.Model} type
+ @param {DS.Model} record
+ @param {any} payload
+ */
+ didUpdateRecord: function() {
+ this.didSaveRecord.apply(this, arguments);
+ },
+
+ /**
+ Acknowledges that the adapter has finished deleting a record.
+
+ Your adapter should call this method from `deleteRecord` when it
+ has deleted a record from its persistent storage and received an
+ acknowledgement.
+
+ If the persistent storage returns a new payload in response to the
+ deletion, pass the payload as the fourth parameter.
+
+ @method didDeleteRecord
+ @param {DS.Store} store
+ @param {subclass of DS.Model} type
+ @param {DS.Model} record
+ @param {any} payload
+ */
+ didDeleteRecord: function() {
+ this.didSaveRecord.apply(this, arguments);
+ },
+
+ /**
+ Acknowledges that the adapter has finished updating or deleting
+ multiple records.
+
+ Your adapter should call this method from its `updateRecords` or
+ `deleteRecords` when it has updated or deleted multiple records
+ to its persistent storage and received an acknowledgement.
+
+ If the persistent storage returns a new payload in response to the
+ creation, pass the payload as the fourth parameter.
+
+ @method didSaveRecords
+ @param {DS.Store} store
+ @param {subclass of DS.Model} type
+ @param {DS.Model} records
+ @param {any} payload
+ */
+ didSaveRecords: function(store, type, records, payload) {
+ records.forEach(function(record) {
+ store.didSaveRecord(record);
+ }, this);
+
+ if (payload) {
+ var loader = DS.loaderFor(store);
+ get(this, 'serializer').extractMany(loader, payload, type);
+ }
+ },
+
+ /**
+ Acknowledges that the adapter has finished updating multiple records.
+
+ Your adapter should call this method from its `updateRecords` when
+ it has updated multiple records to its persistent storage and
+ received an acknowledgement.
+
+ If the persistent storage returns a new payload in response to the
+ update, pass the payload as the fourth parameter.
+
+ @method didUpdateRecords
+ @param {DS.Store} store
+ @param {subclass of DS.Model} type
+ @param {DS.Model} records
+ @param {any} payload
+ */
+ didUpdateRecords: function() {
+ this.didSaveRecords.apply(this, arguments);
+ },
+
+ /**
+ Acknowledges that the adapter has finished updating multiple records.
+
+ Your adapter should call this method from its `deleteRecords` when
+ it has deleted multiple records to its persistent storage and
+ received an acknowledgement.
+
+ If the persistent storage returns a new payload in response to the
+ deletion, pass the payload as the fourth parameter.
+
+ @method didDeleteRecords
+ @param {DS.Store} store
+ @param {subclass of DS.Model} type
+ @param {DS.Model} records
+ @param {any} payload
+ */
+ didDeleteRecords: function() {
+ this.didSaveRecords.apply(this, arguments);
+ },
+
+ /**
+ Loads the response to a request for a record by ID.
+
+ Your adapter should call this method from its `find` method
+ with the response from the backend.
+
+ You should pass the same ID to this method that was given
+ to your find method so that the store knows which record
+ to associate the new data with.
+
+ @method didFindRecord
+ @param {DS.Store} store
+ @param {subclass of DS.Model} type
+ @param {any} payload
+ @param {String} id
+ */
+ didFindRecord: function(store, type, payload, id) {
+ var loader = DS.loaderFor(store);
+
+ loader.load = function(type, data, prematerialized) {
+ prematerialized = prematerialized || {};
+ prematerialized.id = id;
+
+ return store.load(type, data, prematerialized);
+ };
+
+ get(this, 'serializer').extract(loader, payload, type);
+ },
+
+ /**
+ Loads the response to a request for all records by type.
+
+ You adapter should call this method from its `findAll`
+ method with the response from the backend.
+
+ @method didFindAll
+ @param {DS.Store} store
+ @param {subclass of DS.Model} type
+ @param {any} payload
+ */
+ didFindAll: function(store, type, payload) {
+ var loader = DS.loaderFor(store),
+ serializer = get(this, 'serializer');
+
+ store.didUpdateAll(type);
+
+ serializer.extractMany(loader, payload, type);
+ },
+
+ /**
+ Loads the response to a request for records by query.
+
+ Your adapter should call this method from its `findQuery`
+ method with the response from the backend.
+
+ @method didFindQuery
+ @param {DS.Store} store
+ @param {subclass of DS.Model} type
+ @param {any} payload
+ @param {DS.AdapterPopulatedRecordArray} recordArray
+ */
+ didFindQuery: function(store, type, payload, recordArray) {
+ var loader = DS.loaderFor(store);
+
+ loader.populateArray = function(data) {
+ recordArray.load(data);
+ };
+
+ get(this, 'serializer').extractMany(loader, payload, type);
+ },
+
+ /**
+ Loads the response to a request for many records by ID.
+
+ You adapter should call this method from its `findMany`
+ method with the response from the backend.
+
+ @method didFindMany
+ @param {DS.Store} store
+ @param {subclass of DS.Model} type
+ @param {any} payload
+ */
+ didFindMany: function(store, type, payload) {
+ var loader = DS.loaderFor(store);
+
+ get(this, 'serializer').extractMany(loader, payload, type);
+ },
+
+ /**
+ Notifies the store that a request to the backend returned
+ an error.
+
+ Your adapter should call this method to indicate that the
+ backend returned an error for a request.
+
+ @method didError
+ @param {DS.Store} store
+ @param {subclass of DS.Model} type
+ @param {DS.Model} record
+ */
+ didError: function(store, type, record) {
+ store.recordWasError(record);
+ },
+
+ dirtyRecordsForAttributeChange: function(dirtySet, record, attributeName, newValue, oldValue) {
+ if (newValue !== oldValue) {
+ // If this record is embedded, add its parent
+ // to the dirty set.
+ this.dirtyRecordsForRecordChange(dirtySet, record);
+ }
+ },
+
+ dirtyRecordsForRecordChange: function(dirtySet, record) {
+ dirtySet.add(record);
+ },
+
+ dirtyRecordsForBelongsToChange: function(dirtySet, child) {
+ this.dirtyRecordsForRecordChange(dirtySet, child);
+ },
+
+ dirtyRecordsForHasManyChange: function(dirtySet, parent) {
+ this.dirtyRecordsForRecordChange(dirtySet, parent);
+ },
+
+ /**
+ @private
+
+ This method recursively climbs the superclass hierarchy and
+ registers any class-registered transforms on the adapter's
+ serializer.
+
+ Once it registers a transform for a given type, it ignores
+ subsequent transforms for the same attribute type.
+
+ @method registerSerializerTransforms
+ @param {Class} klass the DS.Adapter subclass to extract the
+ transforms from
+ @param {DS.Serializer} serializer the serializer to register
+ the transforms onto
+ @param {Object} seen a hash of attributes already seen
+ */
+ registerSerializerTransforms: function(klass, serializer, seen) {
+ var transforms = klass._registeredTransforms, superclass, prop;
+ var enumTransforms = klass._registeredEnumTransforms;
+
+ for (prop in transforms) {
+ if (!transforms.hasOwnProperty(prop) || prop in seen) { continue; }
+ seen[prop] = true;
+
+ serializer.registerTransform(prop, transforms[prop]);
+ }
+
+ for (prop in enumTransforms) {
+ if (!enumTransforms.hasOwnProperty(prop) || prop in seen) { continue; }
+ seen[prop] = true;
+
+ serializer.registerEnumTransform(prop, enumTransforms[prop]);
+ }
+
+ if (superclass = klass.superclass) {
+ this.registerSerializerTransforms(superclass, serializer, seen);
+ }
+ },
+
+ /**
+ @private
+
+ This method recursively climbs the superclass hierarchy and
+ registers any class-registered mappings on the adapter's
+ serializer.
+
+ @method registerSerializerMappings
+ @param {Class} klass the DS.Adapter subclass to extract the
+ transforms from
+ @param {DS.Serializer} serializer the serializer to register the
+ mappings onto
+ */
+ registerSerializerMappings: function(serializer) {
+ var mappings = this._attributesMap,
+ configurations = this._configurationsMap;
+
+ mappings.forEach(serializer.map, serializer);
+ configurations.forEach(serializer.configure, serializer);
+ },
+
+ /**
+ The `find()` method is invoked when the store is asked for a record that
+ has not previously been loaded. In response to `find()` being called, you
+ should query your persistence layer for a record with the given ID. Once
+ found, you can asynchronously call the store's `load()` method to load
+ the record.
+
+ Here is an example `find` implementation:
+
+ find: function(store, type, id) {
+ var url = type.url;
+ url = url.fmt(id);
+
+ jQuery.getJSON(url, function(data) {
+ // data is a hash of key/value pairs. If your server returns a
+ // root, simply do something like:
+ // store.load(type, id, data.person)
+ store.load(type, id, data);
+ });
+ }
+
+ @method find
+ */
+ find: null,
+
+ serializer: DS.JSONSerializer,
+
+ registerTransform: function(attributeType, transform) {
+ get(this, 'serializer').registerTransform(attributeType, transform);
+ },
+
+ /**
+ A public method that allows you to register an enumerated
+ type on your adapter. This is useful if you want to utilize
+ a text representation of an integer value.
+
+ Eg: Say you want to utilize "low","medium","high" text strings
+ in your app, but you want to persist those as 0,1,2 in your backend.
+ You would first register the transform on your adapter instance:
+
+ adapter.registerEnumTransform('priority', ['low', 'medium', 'high']);
+
+ You would then refer to the 'priority' DS.attr in your model:
+
+ App.Task = DS.Model.extend({
+ priority: DS.attr('priority')
+ });
+
+ And lastly, you would set/get the text representation on your model instance,
+ but the transformed result will be the index number of the type.
+
+ App: myTask.get('priority') => 'low'
+ Server Response / Load: { myTask: {priority: 0} }
+
+ @method registerEnumTransform
+ @param {String} type of the transform
+ @param {Array} array of String objects to use for the enumerated values.
+ This is an ordered list and the index values will be used for the transform.
+ */
+ registerEnumTransform: function(attributeType, objects) {
+ get(this, 'serializer').registerEnumTransform(attributeType, objects);
+ },
+
+ /**
+ If the globally unique IDs for your records should be generated on the client,
+ implement the `generateIdForRecord()` method. This method will be invoked
+ each time you create a new record, and the value returned from it will be
+ assigned to the record's `primaryKey`.
+
+ Most traditional REST-like HTTP APIs will not use this method. Instead, the ID
+ of the record will be set by the server, and your adapter will update the store
+ with the new ID when it calls `didCreateRecord()`. Only implement this method if
+ you intend to generate record IDs on the client-side.
+
+ The `generateIdForRecord()` method will be invoked with the requesting store as
+ the first parameter and the newly created record as the second parameter:
+
+ generateIdForRecord: function(store, record) {
+ var uuid = App.generateUUIDWithStatisticallyLowOddsOfCollision();
+ return uuid;
+ }
+
+ @method generateIdForRecord
+ @param {DS.Store} store
+ @param {DS.Model} record
+ */
+ generateIdForRecord: null,
+
+ materialize: function(record, data, prematerialized) {
+ get(this, 'serializer').materialize(record, data, prematerialized);
+ },
+
+ serialize: function(record, options) {
+ return get(this, 'serializer').serialize(record, options);
+ },
+
+ extractId: function(type, data) {
+ return get(this, 'serializer').extractId(type, data);
+ },
+
+ groupByType: function(enumerable) {
+ var map = Ember.MapWithDefault.create({
+ defaultValue: function() { return Ember.OrderedSet.create(); }
+ });
+
+ enumerable.forEach(function(item) {
+ map.get(item.constructor).add(item);
+ });
+
+ return map;
+ },
+
+ commit: function(store, commitDetails) {
+ this.save(store, commitDetails);
+ },
+
+ save: function(store, commitDetails) {
+ var adapter = this;
+
+ function filter(records) {
+ var filteredSet = Ember.OrderedSet.create();
+
+ records.forEach(function(record) {
+ if (adapter.shouldSave(record)) {
+ filteredSet.add(record);
+ }
+ });
+
+ return filteredSet;
+ }
+
+ this.groupByType(commitDetails.created).forEach(function(type, set) {
+ this.createRecords(store, type, filter(set));
+ }, this);
+
+ this.groupByType(commitDetails.updated).forEach(function(type, set) {
+ this.updateRecords(store, type, filter(set));
+ }, this);
+
+ this.groupByType(commitDetails.deleted).forEach(function(type, set) {
+ this.deleteRecords(store, type, filter(set));
+ }, this);
+ },
+
+ shouldSave: Ember.K,
+
+ createRecords: function(store, type, records) {
+ records.forEach(function(record) {
+ this.createRecord(store, type, record);
+ }, this);
+ },
+
+ updateRecords: function(store, type, records) {
+ records.forEach(function(record) {
+ this.updateRecord(store, type, record);
+ }, this);
+ },
+
+ deleteRecords: function(store, type, records) {
+ records.forEach(function(record) {
+ this.deleteRecord(store, type, record);
+ }, this);
+ },
+
+ findMany: function(store, type, ids) {
+ ids.forEach(function(id) {
+ this.find(store, type, id);
+ }, this);
+ }
+});
+
+DS.Adapter.reopenClass({
+ registerTransform: function(attributeType, transform) {
+ var registeredTransforms = this._registeredTransforms || {};
+
+ registeredTransforms[attributeType] = transform;
+
+ this._registeredTransforms = registeredTransforms;
+ },
+
+ registerEnumTransform: function(attributeType, objects) {
+ var registeredEnumTransforms = this._registeredEnumTransforms || {};
+
+ registeredEnumTransforms[attributeType] = objects;
+
+ this._registeredEnumTransforms = registeredEnumTransforms;
+ },
+
+ map: DS._Mappable.generateMapFunctionFor('attributes', function(key, newValue, map) {
+ var existingValue = map.get(key);
+
+ merge(existingValue, newValue);
+ }),
+
+ configure: DS._Mappable.generateMapFunctionFor('configurations', function(key, newValue, map) {
+ var existingValue = map.get(key);
+
+ // If a mapping configuration is provided, peel it off and apply it
+ // using the DS.Adapter.map API.
+ var mappings = newValue && newValue.mappings;
+ if (mappings) {
+ this.map(key, mappings);
+ delete newValue.mappings;
+ }
+
+ merge(existingValue, newValue);
+ }),
+
+ resolveMapConflict: function(oldValue, newValue) {
+ merge(newValue, oldValue);
+
+ return newValue;
+ }
+});
+
+})();
+
+
+
+(function() {
+/**
+ @module data
+ @submodule data-serializers
+*/
+
+/**
+ @class FixtureSerializer
+ @constructor
+ @namespace DS
+ @extends DS.Serializer
+*/
+
+var get = Ember.get, set = Ember.set;
+
+DS.FixtureSerializer = DS.Serializer.extend({
+ deserializeValue: function(value, attributeType) {
+ return value;
+ },
+
+ serializeValue: function(value, attributeType) {
+ return value;
+ },
+
+ addId: function(data, key, id) {
+ data[key] = id;
+ },
+
+ addAttribute: function(hash, key, value) {
+ hash[key] = value;
+ },
+
+ addBelongsTo: function(hash, record, key, relationship) {
+ var id = get(record, relationship.key+'.id');
+ if (!Ember.isNone(id)) { hash[key] = id; }
+ },
+
+ addHasMany: function(hash, record, key, relationship) {
+ var ids = get(record, relationship.key).map(function(item) {
+ return item.get('id');
+ });
+
+ hash[relationship.key] = ids;
+ },
+
+ extract: function(loader, fixture, type, record) {
+ if (record) { loader.updateId(record, fixture); }
+ this.extractRecordRepresentation(loader, type, fixture);
+ },
+
+ extractMany: function(loader, fixtures, type, records) {
+ var objects = fixtures, references = [];
+ if (records) { records = records.toArray(); }
+
+ for (var i = 0; i < objects.length; i++) {
+ if (records) { loader.updateId(records[i], objects[i]); }
+ var reference = this.extractRecordRepresentation(loader, type, objects[i]);
+ references.push(reference);
+ }
+
+ loader.populateArray(references);
+ },
+
+ extractId: function(type, hash) {
+ var primaryKey = this._primaryKey(type);
+
+ if (hash.hasOwnProperty(primaryKey)) {
+ // Ensure that we coerce IDs to strings so that record
+ // IDs remain consistent between application runs; especially
+ // if the ID is serialized and later deserialized from the URL,
+ // when type information will have been lost.
+ return hash[primaryKey]+'';
+ } else {
+ return null;
+ }
+ },
+
+ extractAttribute: function(type, hash, attributeName) {
+ var key = this._keyForAttributeName(type, attributeName);
+ return hash[key];
+ },
+
+ extractHasMany: function(type, hash, key) {
+ return hash[key];
+ },
+
+ extractBelongsTo: function(type, hash, key) {
+ var val = hash[key];
+ if (val != null) {
+ val = val + '';
+ }
+ return val;
+ },
+
+ extractBelongsToPolymorphic: function(type, hash, key) {
+ var keyForId = this.keyForPolymorphicId(key),
+ keyForType,
+ id = hash[keyForId];
+
+ if (id) {
+ keyForType = this.keyForPolymorphicType(key);
+ return {id: id, type: hash[keyForType]};
+ }
+
+ return null;
+ },
+
+ keyForPolymorphicId: function(key) {
+ return key;
+ },
+
+ keyForPolymorphicType: function(key) {
+ return key + '_type';
+ }
+});
+
+})();
+
+
+
+(function() {
+/**
+ @module data
+ @submodule data-adapters
+*/
+
+var get = Ember.get, fmt = Ember.String.fmt,
+ dump = Ember.get(window, 'JSON.stringify') || function(object) { return object.toString(); };
+
+/**
+ `DS.FixtureAdapter` is an adapter that loads records from memory.
+ Its primarily used for development and testing. You can also use
+ `DS.FixtureAdapter` while working on the API but are not ready to
+ integrate yet. It is a fully functioning adapter. All CRUD methods
+ are implemented. You can also implement query logic that a remote
+ system would do. Its possible to do develop your entire application
+ with `DS.FixtureAdapter`.
+
+ @class FixtureAdapter
+ @constructor
+ @namespace DS
+ @extends DS.Adapter
+*/
+DS.FixtureAdapter = DS.Adapter.extend({
+
+ simulateRemoteResponse: true,
+
+ latency: 50,
+
+ serializer: DS.FixtureSerializer,
+
+ /*
+ Implement this method in order to provide data associated with a type
+ */
+ fixturesForType: function(type) {
+ if (type.FIXTURES) {
+ var fixtures = Ember.A(type.FIXTURES);
+ return fixtures.map(function(fixture){
+ var fixtureIdType = typeof fixture.id;
+ if(fixtureIdType !== "number" && fixtureIdType !== "string"){
+ throw new Error(fmt('the id property must be defined as a number or string for fixture %@', [dump(fixture)]));
+ }
+ fixture.id = fixture.id + '';
+ return fixture;
+ });
+ }
+ return null;
+ },
+
+ /*
+ Implement this method in order to query fixtures data
+ */
+ queryFixtures: function(fixtures, query, type) {
+ Ember.assert('Not implemented: You must override the DS.FixtureAdapter::queryFixtures method to support querying the fixture store.');
+ },
+
+ updateFixtures: function(type, fixture) {
+ if(!type.FIXTURES) {
+ type.FIXTURES = [];
+ }
+
+ var fixtures = type.FIXTURES;
+
+ this.deleteLoadedFixture(type, fixture);
+
+ fixtures.push(fixture);
+ },
+
+ /*
+ Implement this method in order to provide provide json for CRUD methods
+ */
+ mockJSON: function(type, record) {
+ return this.serialize(record, { includeId: true });
+ },
+
+ /*
+ Adapter methods
+ */
+ generateIdForRecord: function(store, record) {
+ return Ember.guidFor(record);
+ },
+
+ find: function(store, type, id) {
+ var fixtures = this.fixturesForType(type),
+ fixture;
+
+ Ember.warn("Unable to find fixtures for model type " + type.toString(), fixtures);
+
+ if (fixtures) {
+ fixture = Ember.A(fixtures).findProperty('id', id);
+ }
+
+ if (fixture) {
+ this.simulateRemoteCall(function() {
+ this.didFindRecord(store, type, fixture, id);
+ }, this);
+ }
+ },
+
+ findMany: function(store, type, ids) {
+ var fixtures = this.fixturesForType(type);
+
+ Ember.assert("Unable to find fixtures for model type "+type.toString(), !!fixtures);
+
+ if (fixtures) {
+ fixtures = fixtures.filter(function(item) {
+ return ids.indexOf(item.id) !== -1;
+ });
+ }
+
+ if (fixtures) {
+ this.simulateRemoteCall(function() {
+ this.didFindMany(store, type, fixtures);
+ }, this);
+ }
+ },
+
+ findAll: function(store, type) {
+ var fixtures = this.fixturesForType(type);
+
+ Ember.assert("Unable to find fixtures for model type "+type.toString(), !!fixtures);
+
+ this.simulateRemoteCall(function() {
+ this.didFindAll(store, type, fixtures);
+ }, this);
+ },
+
+ findQuery: function(store, type, query, array) {
+ var fixtures = this.fixturesForType(type);
+
+ Ember.assert("Unable to find fixtures for model type "+type.toString(), !!fixtures);
+
+ fixtures = this.queryFixtures(fixtures, query, type);
+
+ if (fixtures) {
+ this.simulateRemoteCall(function() {
+ this.didFindQuery(store, type, fixtures, array);
+ }, this);
+ }
+ },
+
+ createRecord: function(store, type, record) {
+ var fixture = this.mockJSON(type, record);
+
+ this.updateFixtures(type, fixture);
+
+ this.simulateRemoteCall(function() {
+ this.didCreateRecord(store, type, record, fixture);
+ }, this);
+ },
+
+ updateRecord: function(store, type, record) {
+ var fixture = this.mockJSON(type, record);
+
+ this.updateFixtures(type, fixture);
+
+ this.simulateRemoteCall(function() {
+ this.didUpdateRecord(store, type, record, fixture);
+ }, this);
+ },
+
+ deleteRecord: function(store, type, record) {
+ var fixture = this.mockJSON(type, record);
+
+ this.deleteLoadedFixture(type, fixture);
+
+ this.simulateRemoteCall(function() {
+ this.didDeleteRecord(store, type, record);
+ }, this);
+ },
+
+ /*
+ @private
+ */
+ deleteLoadedFixture: function(type, record) {
+ var existingFixture = this.findExistingFixture(type, record);
+
+ if(existingFixture) {
+ var index = type.FIXTURES.indexOf(existingFixture);
+ type.FIXTURES.splice(index, 1);
+ return true;
+ }
+ },
+
+ findExistingFixture: function(type, record) {
+ var fixtures = this.fixturesForType(type);
+ var id = this.extractId(type, record);
+
+ return this.findFixtureById(fixtures, id);
+ },
+
+ findFixtureById: function(fixtures, id) {
+ return Ember.A(fixtures).find(function(r) {
+ if(''+get(r, 'id') === ''+id) {
+ return true;
+ } else {
+ return false;
+ }
+ });
+ },
+
+ simulateRemoteCall: function(callback, context) {
+ if (get(this, 'simulateRemoteResponse')) {
+ // Schedule with setTimeout
+ Ember.run.later(context, callback, get(this, 'latency'));
+ } else {
+ // Asynchronous, but at the of the runloop with zero latency
+ Ember.run.once(context, callback);
+ }
+ }
+});
+
+})();
+
+
+
+(function() {
+/**
+ @module data
+ @submodule data-serializers
+*/
+
+/**
+ @class RESTSerializer
+ @constructor
+ @namespace DS
+ @extends DS.Serializer
+*/
+
+var get = Ember.get;
+
+DS.RESTSerializer = DS.JSONSerializer.extend({
+ keyForAttributeName: function(type, name) {
+ return Ember.String.decamelize(name);
+ },
+
+ keyForBelongsTo: function(type, name) {
+ var key = this.keyForAttributeName(type, name);
+
+ if (this.embeddedType(type, name)) {
+ return key;
+ }
+
+ return key + "_id";
+ },
+
+ keyForHasMany: function(type, name) {
+ var key = this.keyForAttributeName(type, name);
+
+ if (this.embeddedType(type, name)) {
+ return key;
+ }
+
+ return this.singularize(key) + "_ids";
+ },
+
+ keyForPolymorphicId: function(key) {
+ return key;
+ },
+
+ keyForPolymorphicType: function(key) {
+ return key.replace(/_id$/, '_type');
+ },
+
+ extractValidationErrors: function(type, json) {
+ var errors = {};
+
+ get(type, 'attributes').forEach(function(name) {
+ var key = this._keyForAttributeName(type, name);
+ if (json['errors'].hasOwnProperty(key)) {
+ errors[name] = json['errors'][key];
+ }
+ }, this);
+
+ return errors;
+ }
+});
+
+})();
+
+
+
+(function() {
+/**
+ @module data
+ @submodule data-adapters
+*/
+
+var get = Ember.get, set = Ember.set;
+
+DS.rejectionHandler = function(reason) {
+ Ember.Logger.assert([reason, reason.message, reason.stack]);
+
+ throw reason;
+};
+
+/**
+ The REST adapter allows your store to communicate with an HTTP server by
+ transmitting JSON via XHR. Most Ember.js apps that consume a JSON API
+ should use the REST adapter.
+
+ This adapter is designed around the idea that the JSON exchanged with
+ the server should be conventional.
+
+ ## JSON Structure
+
+ The REST adapter expects the JSON returned from your server to follow
+ these conventions.
+
+ ### Object Root
+
+ The JSON payload should be an object that contains the record inside a
+ root property. For example, in response to a `GET` request for
+ `/posts/1`, the JSON should look like this:
+
+ ```js
+ {
+ "post": {
+ title: "I'm Running to Reform the W3C's Tag",
+ author: "Yehuda Katz"
+ }
+ }
+ ```
+
+ ### Conventional Names
+
+ Attribute names in your JSON payload should be the underscored versions of
+ the attributes in your Ember.js models.
+
+ For example, if you have a `Person` model:
+
+ ```js
+ App.Person = DS.Model.extend({
+ firstName: DS.attr('string'),
+ lastName: DS.attr('string'),
+ occupation: DS.attr('string')
+ });
+ ```
+
+ The JSON returned should look like this:
+
+ ```js
+ {
+ "person": {
+ "first_name": "Barack",
+ "last_name": "Obama",
+ "occupation": "President"
+ }
+ }
+ ```
+
+ @class RESTAdapter
+ @constructor
+ @namespace DS
+ @extends DS.Adapter
+*/
+DS.RESTAdapter = DS.Adapter.extend({
+ namespace: null,
+ bulkCommit: false,
+ since: 'since',
+
+ serializer: DS.RESTSerializer,
+
+ init: function() {
+ this._super.apply(this, arguments);
+ },
+
+ shouldSave: function(record) {
+ var reference = get(record, '_reference');
+
+ return !reference.parent;
+ },
+
+ dirtyRecordsForRecordChange: function(dirtySet, record) {
+ this._dirtyTree(dirtySet, record);
+ },
+
+ dirtyRecordsForHasManyChange: function(dirtySet, record, relationship) {
+ var embeddedType = get(this, 'serializer').embeddedType(record.constructor, relationship.secondRecordName);
+
+ if (embeddedType === 'always') {
+ relationship.childReference.parent = relationship.parentReference;
+ this._dirtyTree(dirtySet, record);
+ }
+ },
+
+ _dirtyTree: function(dirtySet, record) {
+ dirtySet.add(record);
+
+ get(this, 'serializer').eachEmbeddedRecord(record, function(embeddedRecord, embeddedType) {
+ if (embeddedType !== 'always') { return; }
+ if (dirtySet.has(embeddedRecord)) { return; }
+ this._dirtyTree(dirtySet, embeddedRecord);
+ }, this);
+
+ var reference = record.get('_reference');
+
+ if (reference.parent) {
+ var store = get(record, 'store');
+ var parent = store.recordForReference(reference.parent);
+ this._dirtyTree(dirtySet, parent);
+ }
+ },
+
+ createRecord: function(store, type, record) {
+ var root = this.rootForType(type);
+ var adapter = this;
+ var data = {};
+
+ data[root] = this.serialize(record, { includeId: true });
+
+ return this.ajax(this.buildURL(root), "POST", {
+ data: data
+ }).then(function(json){
+ adapter.didCreateRecord(store, type, record, json);
+ }, function(xhr) {
+ adapter.didError(store, type, record, xhr);
+ throw xhr;
+ }).then(null, DS.rejectionHandler);
+ },
+
+ createRecords: function(store, type, records) {
+ var adapter = this;
+
+ if (get(this, 'bulkCommit') === false) {
+ return this._super(store, type, records);
+ }
+
+ var root = this.rootForType(type),
+ plural = this.pluralize(root);
+
+ var data = {};
+ data[plural] = [];
+ records.forEach(function(record) {
+ data[plural].push(this.serialize(record, { includeId: true }));
+ }, this);
+
+ return this.ajax(this.buildURL(root), "POST", {
+ data: data
+ }).then(function(json) {
+ adapter.didCreateRecords(store, type, records, json);
+ }).then(null, DS.rejectionHandler);
+ },
+
+ updateRecord: function(store, type, record) {
+ var id, root, adapter, data;
+
+ id = get(record, 'id');
+ root = this.rootForType(type);
+ adapter = this;
+
+ data = {};
+ data[root] = this.serialize(record);
+
+ return this.ajax(this.buildURL(root, id), "PUT",{
+ data: data
+ }).then(function(json){
+ adapter.didUpdateRecord(store, type, record, json);
+ }, function(xhr) {
+ adapter.didError(store, type, record, xhr);
+ throw xhr;
+ }).then(null, DS.rejectionHandler);
+ },
+
+ updateRecords: function(store, type, records) {
+ var root, plural, adapter, data;
+
+ if (get(this, 'bulkCommit') === false) {
+ return this._super(store, type, records);
+ }
+
+ root = this.rootForType(type);
+ plural = this.pluralize(root);
+ adapter = this;
+
+ data = {};
+
+ data[plural] = [];
+
+ records.forEach(function(record) {
+ data[plural].push(this.serialize(record, { includeId: true }));
+ }, this);
+
+ return this.ajax(this.buildURL(root, "bulk"), "PUT", {
+ data: data
+ }).then(function(json) {
+ adapter.didUpdateRecords(store, type, records, json);
+ }).then(null, DS.rejectionHandler);
+ },
+
+ deleteRecord: function(store, type, record) {
+ var id, root, adapter;
+
+ id = get(record, 'id');
+ root = this.rootForType(type);
+ adapter = this;
+
+ return this.ajax(this.buildURL(root, id), "DELETE").then(function(json){
+ adapter.didDeleteRecord(store, type, record, json);
+ }, function(xhr){
+ adapter.didError(store, type, record, xhr);
+ throw xhr;
+ }).then(null, DS.rejectionHandler);
+ },
+
+ deleteRecords: function(store, type, records) {
+ var root, plural, serializer, adapter, data;
+
+ if (get(this, 'bulkCommit') === false) {
+ return this._super(store, type, records);
+ }
+
+ root = this.rootForType(type);
+ plural = this.pluralize(root);
+ serializer = get(this, 'serializer');
+ adapter = this;
+
+ data = {};
+
+ data[plural] = [];
+ records.forEach(function(record) {
+ data[plural].push(serializer.serializeId( get(record, 'id') ));
+ });
+
+ return this.ajax(this.buildURL(root, 'bulk'), "DELETE", {
+ data: data
+ }).then(function(json){
+ adapter.didDeleteRecords(store, type, records, json);
+ }).then(null, DS.rejectionHandler);
+ },
+
+ find: function(store, type, id) {
+ var root = this.rootForType(type), adapter = this;
+
+ return this.ajax(this.buildURL(root, id), "GET").
+ then(function(json){
+ adapter.didFindRecord(store, type, json, id);
+ }).then(null, DS.rejectionHandler);
+ },
+
+ findAll: function(store, type, since) {
+ var root, adapter;
+
+ root = this.rootForType(type);
+ adapter = this;
+
+ return this.ajax(this.buildURL(root), "GET",{
+ data: this.sinceQuery(since)
+ }).then(function(json) {
+ adapter.didFindAll(store, type, json);
+ }).then(null, DS.rejectionHandler);
+ },
+
+ findQuery: function(store, type, query, recordArray) {
+ var root = this.rootForType(type),
+ adapter = this;
+
+ return this.ajax(this.buildURL(root), "GET", {
+ data: query
+ }).then(function(json){
+ adapter.didFindQuery(store, type, json, recordArray);
+ }).then(null, DS.rejectionHandler);
+ },
+
+ findMany: function(store, type, ids, owner) {
+ var root = this.rootForType(type),
+ adapter = this;
+
+ ids = this.serializeIds(ids);
+
+ return this.ajax(this.buildURL(root), "GET", {
+ data: {ids: ids}
+ }).then(function(json) {
+ adapter.didFindMany(store, type, json);
+ }).then(null, DS.rejectionHandler);
+ },
+
+ /**
+ @private
+
+ This method serializes a list of IDs using `serializeId`
+
+ @return {Array} an array of serialized IDs
+ */
+ serializeIds: function(ids) {
+ var serializer = get(this, 'serializer');
+
+ return Ember.EnumerableUtils.map(ids, function(id) {
+ return serializer.serializeId(id);
+ });
+ },
+
+ didError: function(store, type, record, xhr) {
+ if (xhr.status === 422) {
+ var json = JSON.parse(xhr.responseText),
+ serializer = get(this, 'serializer'),
+ errors = serializer.extractValidationErrors(type, json);
+
+ store.recordWasInvalid(record, errors);
+ } else {
+ this._super.apply(this, arguments);
+ }
+ },
+
+ ajax: function(url, type, hash) {
+ var adapter = this;
+
+ return new Ember.RSVP.Promise(function(resolve, reject) {
+ hash = hash || {};
+ hash.url = url;
+ hash.type = type;
+ hash.dataType = 'json';
+ hash.context = adapter;
+
+ if (hash.data && type !== 'GET') {
+ hash.contentType = 'application/json; charset=utf-8';
+ hash.data = JSON.stringify(hash.data);
+ }
+
+ hash.success = function(json) {
+ Ember.run(null, resolve, json);
+ };
+
+ hash.error = function(jqXHR, textStatus, errorThrown) {
+ Ember.run(null, reject, jqXHR);
+ };
+
+ Ember.$.ajax(hash);
+ });
+ },
+
+ url: "",
+
+ rootForType: function(type) {
+ var serializer = get(this, 'serializer');
+ return serializer.rootForType(type);
+ },
+
+ pluralize: function(string) {
+ var serializer = get(this, 'serializer');
+ return serializer.pluralize(string);
+ },
+
+ buildURL: function(record, suffix) {
+ var url = [this.url];
+
+ Ember.assert("Namespace URL (" + this.namespace + ") must not start with slash", !this.namespace || this.namespace.toString().charAt(0) !== "/");
+ Ember.assert("Record URL (" + record + ") must not start with slash", !record || record.toString().charAt(0) !== "/");
+ Ember.assert("URL suffix (" + suffix + ") must not start with slash", !suffix || suffix.toString().charAt(0) !== "/");
+
+ if (!Ember.isNone(this.namespace)) {
+ url.push(this.namespace);
+ }
+
+ url.push(this.pluralize(record));
+ if (suffix !== undefined) {
+ url.push(suffix);
+ }
+
+ return url.join("/");
+ },
+
+ sinceQuery: function(since) {
+ var query = {};
+ query[get(this, 'since')] = since;
+ return since ? query : null;
+ }
+});
+
+})();
+
+
+
+(function() {
+/**
+ Adapters included with Ember-Data.
+
+ @module data
+ @submodule data-adapters
+
+ @main data-adapters
+*/
+
+})();
+
+
+
+(function() {
+//Copyright (C) 2011 by Living Social, Inc.
+
+//Permission is hereby granted, free of charge, to any person obtaining a copy of
+//this software and associated documentation files (the "Software"), to deal in
+//the Software without restriction, including without limitation the rights to
+//use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+//of the Software, and to permit persons to whom the Software is furnished to do
+//so, subject to the following conditions:
+
+//The above copyright notice and this permission notice shall be included in all
+//copies or substantial portions of the Software.
+
+//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+//SOFTWARE.
+
+/**
+ Ember Data
+ @module data
+*/
+
+})();
+
+
+})();