diff options
Diffstat (limited to 'public/js/libs/new-ember-data.js')
-rw-r--r-- | public/js/libs/new-ember-data.js | 8431 |
1 files changed, 0 insertions, 8431 deletions
diff --git a/public/js/libs/new-ember-data.js b/public/js/libs/new-ember-data.js deleted file mode 100644 index e07c21b..0000000 --- a/public/js/libs/new-ember-data.js +++ /dev/null @@ -1,8431 +0,0 @@ -// Last commit: 57d6c01 (2013-03-18 11:27:29 -0700) - - -(function() { -window.DS = Ember.Namespace.create({ - // this one goes past 11 - CURRENT_API_REVISION: 12 -}); - -})(); - - - -(function() { -var DeferredMixin = Ember.DeferredMixin, // ember-runtime/mixins/deferred - Evented = Ember.Evented, // ember-runtime/mixins/evented - run = Ember.run, // ember-metal/run-loop - get = Ember.get; // ember-metal/accessors - -var LoadPromise = Ember.Mixin.create(Evented, DeferredMixin, { - init: function() { - this._super.apply(this, arguments); - this.one('didLoad', function() { - run(this, 'resolve', 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. -*/ - -DS.RecordArray = Ember.ArrayProxy.extend(Ember.Evented, 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() { -var get = Ember.get; - -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 store = get(this, 'store'); - store.updateRecordArrayFilter(this, get(this, 'type'), get(this, 'filterFunction')); - }, 'filterFunction') -}); - -})(); - - - -(function() { -var get = Ember.get, set = Ember.set; - -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) { - var store = get(this, 'store'), type = get(this, 'type'); - - this.beginPropertyChanges(); - set(this, 'content', Ember.A(references)); - set(this, 'isLoaded', true); - this.endPropertyChanges(); - - var self = this; - // TODO: does triggering didLoad event should be the last action of the runLoop? - Ember.run.once(function() { - self.trigger('didLoad'); - }); - } -}); - -})(); - - - -(function() { -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_. -*/ -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, - - // 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'), - type = get(this, 'type'), - owner = get(this, 'owner'); - - store.fetchUnloadedReferences(type, 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') === record.constructor)); - 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; - - transaction = transaction || get(owner, 'transaction'); - - record = store.createRecord.call(store, type, hash, transaction); - this.pushObject(record); - - return record; - } - -}); - -})(); - - - -(function() { - -})(); - - - -(function() { -var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt, - removeObject = Ember.EnumerableUtils.removeObject, forEach = Ember.EnumerableUtils.forEach; - -/** - 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. -*/ - -var arrayDefault = function() { return []; }; - -DS.Transaction = Ember.Object.extend({ - /** - @private - - Creates the bucket data structure used to segregate records by - type. - */ - init: function() { - set(this, 'buckets', { - clean: Ember.OrderedSet.create(), - created: Ember.OrderedSet.create(), - updated: Ember.OrderedSet.create(), - deleted: Ember.OrderedSet.create(), - inflight: Ember.OrderedSet.create() - }); - - set(this, 'relationships', 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'); - }), - - /** - Adds an existing record to this transaction. Only records without - modficiations (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 recordTransaction = get(record, 'transaction'), - defaultTransaction = get(this, 'store.defaultTransaction'); - - // Make `add` idempotent - if (recordTransaction === this) { return; } - - // XXX it should be possible to move a dirty transaction from the default transaction - - // we could probably make this work if someone has a valid use case. Do you? - Ember.assert("Once a record has changed, you cannot move it into a different transaction", !get(record, 'isDirty')); - - Ember.assert("Models cannot belong to more than one transaction at a time.", recordTransaction === defaultTransaction); - - this.adoptRecord(record); - }, - - relationshipBecameDirty: function(relationship) { - get(this, 'relationships').add(relationship); - }, - - relationshipBecameClean: function(relationship) { - get(this, 'relationships').remove(relationship); - }, - - /** - 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'); - var adapter = get(store, '_adapter'); - var defaultTransaction = get(store, 'defaultTransaction'); - - var iterate = function(records) { - var set = records.copy(); - set.forEach(function (record) { - record.send('willCommit'); - }); - return set; - }; - - var relationships = get(this, 'relationships'); - - var commitDetails = { - created: iterate(this.bucketForType('created')), - updated: iterate(this.bucketForType('updated')), - deleted: iterate(this.bucketForType('deleted')), - relationships: relationships - }; - - if (this === defaultTransaction) { - set(store, 'defaultTransaction', store.transaction()); - } - - this.removeCleanRecords(); - - if (!commitDetails.created.isEmpty() || !commitDetails.updated.isEmpty() || !commitDetails.deleted.isEmpty() || !relationships.isEmpty()) { - if (adapter && adapter.commit) { adapter.commit(store, commitDetails); } - else { throw fmt("Adapter is either null or does not implement `commit` method", this); } - } - - // 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() { - // Loop through all of the records in each of the dirty states - // and initiate a rollback on them. As a side effect of telling - // the record to roll back, it should also move itself out of - // the dirty bucket and into the clean bucket. - ['created', 'updated', 'deleted', 'inflight'].forEach(function(bucketType) { - var records = this.bucketForType(bucketType); - forEach(records, function(record) { - record.send('rollback'); - }); - records.clear(); - }, this); - - // Now that all records in the transaction are guaranteed to be - // clean, migrate them all to the store's default transaction. - this.removeCleanRecords(); - }, - - /** - @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 clean = this.bucketForType('clean'); - clean.forEach(function(record) { - this.remove(record); - }, this); - clean.clear(); - }, - - /** - @private - - Returns the bucket for the given bucket type. For example, you might call - `this.bucketForType('updated')` to get the `Ember.Map` that contains all - of the records that have changes pending. - - @param {String} bucketType the type of bucket - @returns Ember.Map - */ - bucketForType: function(bucketType) { - var buckets = get(this, 'buckets'); - - return get(buckets, bucketType); - }, - - /** - @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.removeFromBucket('clean', record); - } - - this.addToBucket('clean', record); - set(record, 'transaction', this); - }, - - /** - @private - - Adds a record to the named bucket. - - @param {String} bucketType one of `clean`, `created`, `updated`, or `deleted` - */ - addToBucket: function(bucketType, record) { - this.bucketForType(bucketType).add(record); - }, - - /** - @private - - Removes a record from the named bucket. - - @param {String} bucketType one of `clean`, `created`, `updated`, or `deleted` - */ - removeFromBucket: function(bucketType, record) { - this.bucketForType(bucketType).remove(record); - }, - - /** - @private - - Called by a record's state manager to indicate that the record has entered - a dirty state. The record will be moved from the `clean` bucket and into - the appropriate dirty bucket. - - @param {String} bucketType one of `created`, `updated`, or `deleted` - */ - recordBecameDirty: function(bucketType, record) { - this.removeFromBucket('clean', record); - this.addToBucket(bucketType, record); - }, - - /** - @private - - Called by a record's state manager to indicate that the record has entered - inflight state. The record will be moved from its current dirty bucket and into - the `inflight` bucket. - - @param {String} bucketType one of `created`, `updated`, or `deleted` - */ - recordBecameInFlight: function(kind, record) { - this.removeFromBucket(kind, record); - this.addToBucket('inflight', record); - }, - - recordIsMoving: function(kind, record) { - this.removeFromBucket(kind, record); - this.addToBucket('clean', record); - }, - - /** - @private - - Called by a record's state manager to indicate that the record has entered - a clean state. The record will be moved from its current dirty or inflight bucket and into - the `clean` bucket. - - @param {String} bucketType one of `created`, `updated`, or `deleted` - */ - recordBecameClean: function(kind, record) { - this.removeFromBucket(kind, record); - this.remove(record); - } -}); - -})(); - - - -(function() { -var classify = Ember.String.classify, get = Ember.get; - -/** -@private - - 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. -**/ - -var resolveMapConflict = function(oldValue, newValue, mappingsKey) { - 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 = Ember.metaPath(this, ['DS.Mappable'], true); - - 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 = Ember.metaPath(klass, ['DS.Mappable'], true); - - 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, mapName); - } - - instanceMap.set(transformedKey, newValue); - } - } - - -}); - -DS._Mappable.generateMapFunctionFor = function(mapName, transform) { - return function(key, value) { - var meta = Ember.metaPath(this, ['DS.Mappable'], true); - var map = meta[mapName] || Ember.MapWithDefault.create({ - defaultValue: function() { return {}; } - }); - - transform.call(this, key, value, map); - - meta[mapName] = map; - }; -}; - -})(); - - - -(function() { -/*globals Ember*/ -/*jshint eqnull:true*/ -var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt, once = Ember.run.once; -var forEach = Ember.EnumerableUtils.forEach; -// 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. -// * +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. -// * +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+''; -}; - -var map = Ember.EnumerableUtils.map; - -/** - The store contains all of the data for records loaded from the server. - It is also responsible for creating instances of DS.Model that wraps - the individual data for a record, so that they can be bound to in your - Handlebars templates. - - Create a new store like this: - - MyApp.store = DS.Store.create(); - - You can retrieve DS.Model instances from the store in several ways. To retrieve - a record for a specific id, use the `find()` method: - - var record = MyApp.store.find(MyApp.Contact, 123); - - 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. -*/ -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() { - // Enforce API revisioning. See BREAKING_CHANGES.md for more. - var revision = get(this, 'revision'); - - if (revision !== DS.CURRENT_API_REVISION && !Ember.ENV.TESTING) { - throw new Error("Error: The Ember Data library has had breaking API changes since the last time you updated the library. Please review the list of breaking changes at https://github.com/emberjs/data/blob/master/BREAKING_CHANGES.md, then update your store's `revision` property to " + DS.CURRENT_API_REVISION); - } - - if (!get(DS, 'defaultStore') || get(this, 'isDefaultStore')) { - set(DS, 'defaultStore', this); - } - - // internal bookkeeping; not observable - this.typeMaps = {}; - this.recordCache = []; - this.clientIdToId = {}; - this.clientIdToType = {}; - this.clientIdToData = {}; - this.clientIdToPrematerializedData = {}; - this.recordArraysByClientId = {}; - this.relationshipChanges = {}; - this.recordReferences = {}; - - // 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. - this.loadingRecordArrays = {}; - - this._recordsToSave = Ember.OrderedSet.create(); - - 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 }); - }, - - 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; - - }, - /** - @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 clientId = get(record, 'clientId'), - cidToData = this.clientIdToData, - adapter = this.adapterForType(record.constructor), - data = cidToData[clientId]; - - cidToData[clientId] = MATERIALIZED; - - var prematerialized = this.clientIdToPrematerializedData[clientId]; - - // Ensures the record's data structures are setup - // before being populated by the adapter. - 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, prematerialized); - } - }, - - /** - @private - - Returns true if there is already a record for this clientId. - - This is used to determine whether cleanup is required, so that - "changes" to unmaterialized records do not trigger mass - materialization. - - For example, if a parent record in a relationship with a large - number of children is deleted, we want to avoid materializing - those children. - - @param {Object} reference - @return {Boolean} - */ - recordIsMaterialized: function(reference) { - return !!this.recordCache[reference.clientId]; - }, - - /** - 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: 'DS.RESTAdapter', - - /** - @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()`. - - @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. - var adapter; - if (Ember.isNone(id)) { - adapter = this.adapterForType(type); - if (adapter && adapter.generateIdForRecord) { - id = coerceId(adapter.generateIdForRecord(this, record)); - properties.id = id; - } - } - - 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 clientId = this.pushData(CREATED, id, type); - - // Now that we have a clientId, attach it to the record we - // just created. - set(record, 'clientId', clientId); - - // Move the record out of its initial `empty` state into - // the `loaded` state. - record.loadedData(); - - // Make sure the data is set up so the record doesn't - // try to materialize its nonexistent data. - record.setupData(); - - // Store the record we just created in the record cache for - // this clientId. - this.recordCache[clientId] = record; - - // 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. - */ - 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 clientId = this.typeMapFor(type).idToCid[id]; - - if (clientId) { - return this.findByClientId(type, clientId); - } - - clientId = this.pushData(LOADING, id, type); - - // create a new instance of the model type in the - // 'isLoading' state - var record = this.materializeRecord(type, clientId, id); - - // let the adapter set the data, possibly async - var adapter = this.adapterForType(type); - if (adapter && adapter.find) { adapter.find(this, type, id); } - else { throw "Adapter is either null or does not implement `find` method"; } - - return record; - }, - - reloadRecord: function(record) { - var type = record.constructor, - adapter = this.adapterForType(type), - 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); - - adapter.find(this, type, id); - }, - - /** - @private - - This method returns a record for a given clientId. - - If there is no record object yet for the clientId, this method materializes - a new record object. This allows adapters to eagerly load large amounts of - data into the store, and avoid incurring the cost to create the objects - until they are requested. - - Several parts of Ember Data call this method: - - * findById, if a clientId already exists for a given type and - id combination - * OneToManyChange, which is backed by clientIds, when getChild, - getOldParent or getNewParent are called - * RecordArray, which is backed by clientIds, when an object at - a particular index is looked up - - In short, it's a convenient way to get a record for a known - clientId, materializing it if necessary. - - @param {Class} type - @param {Number|String} clientId - */ - findByClientId: function(type, clientId) { - var cidToData, record, id; - - record = this.recordCache[clientId]; - - if (!record) { - // create a new instance of the model type in the - // 'isLoading' state - id = this.clientIdToId[clientId]; - record = this.materializeRecord(type, clientId, id); - - cidToData = this.clientIdToData; - - if (typeof cidToData[clientId] === 'object') { - record.loadedData(); - } - } - - return record; - }, - - /** - @private - - Given a type and array of `clientId`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. - */ - neededReferences: function(type, references) { - var neededReferences = [], - cidToData = this.clientIdToData, - reference; - - for (var i=0, l=references.length; i<l; i++) { - reference = references[i]; - - if (cidToData[reference.clientId] === UNLOADED) { - neededReferences.push(reference); - cidToData[reference.clientId] = LOADING; - } - } - - return neededReferences; - }, - - /** - @private - - This method is the entry point that relationships use to update - themselves when their underlying data changes. - - First, it determines which of its `clientId`s are still unloaded, - then converts the needed `clientId`s to IDs and invokes `findMany` - on the adapter. - */ - fetchUnloadedReferences: function(type, references, owner) { - var neededReferences = this.neededReferences(type, references); - this.fetchMany(type, neededReferences, owner); - }, - - /** - @private - - This method takes a type and list of `clientId`s, converts the - `clientId`s into IDs, and then invokes the adapter's `findMany` - method. - - 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(type, references, owner) { - if (!references.length) { return; } - - var ids = map(references, function(reference) { - return reference.id; - }); - - var adapter = this.adapterForType(type); - if (adapter && adapter.findMany) { adapter.findMany(this, type, ids, owner); } - else { throw "Adapter is either null or does not implement `findMany` method"; } - }, - - referenceForId: function(type, id) { - var clientId = this.clientIdForId(type, id); - return this.referenceForClientId(clientId); - }, - - referenceForClientId: function(clientId) { - var references = this.recordReferences; - - if (references[clientId]) { - return references[clientId]; - } - - var type = this.clientIdToType[clientId]; - - return references[clientId] = { - id: this.idForClientId(clientId), - clientId: clientId, - type: type - }; - }, - - recordForReference: function(reference) { - return this.findByClientId(reference.type, reference.clientId); - }, - - /** - @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 clientIds 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, ids, record, relationship) { - // 1. Convert ids to client ids - // 2. Determine which of the client ids need to be loaded - // 3. Create a new ManyArray whose content is ALL of the clientIds - // 4. Decrement the ManyArray's counter by the number of loaded clientIds - // 5. Put the ManyArray into our bookkeeping data structure, keyed on - // the needed clientIds - // 6. Ask the adapter to load the records for the unloaded clientIds (but - // convert them back to ids) - - if (!Ember.isArray(ids)) { - var adapter = this.adapterForType(type); - if (adapter && adapter.findHasMany) { adapter.findHasMany(this, record, relationship, ids); } - else if (ids !== undefined) { throw fmt("Adapter is either null or does not implement `findHasMany` method", this); } - - return this.createManyArray(type, Ember.A()); - } - - // Coerce server IDs into Record Reference - var references = map(ids, function(reference) { - if (typeof reference !== 'object' && reference !== null) { - return this.referenceForId(type, reference); - } - - return reference; - }, this); - - var neededReferences = this.neededReferences(type, references), - manyArray = this.createManyArray(type, Ember.A(references)), - loadingRecordArrays = this.loadingRecordArrays, - reference, clientId, i, l; - - // Start the decrementing counter on the ManyArray at the number of - // records we need to load from the adapter - manyArray.loadingRecordsCount(neededReferences.length); - - if (neededReferences.length) { - for (i=0, l=neededReferences.length; i<l; i++) { - reference = neededReferences[i]; - clientId = reference.clientId; - - // 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. - if (loadingRecordArrays[clientId]) { - loadingRecordArrays[clientId].push(manyArray); - } else { - this.loadingRecordArrays[clientId] = [ manyArray ]; - } - } - - this.fetchMany(type, neededReferences, record); - } else { - // all requested records are available - manyArray.set('isLoaded', true); - - Ember.run.once(function() { - manyArray.trigger('didLoad'); - }); - } - - return manyArray; - }, - - /** - @private - - 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. - - @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); - if (adapter && adapter.findQuery) { adapter.findQuery(this, type, query, array); } - else { throw "Adapter is either null or does not implement `findQuery` method"; } - 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) { - var array = this.all(type); - this.fetchAll(type, array); - return array; - }, - - /** - @private - */ - fetchAll: function(type, array) { - var sinceToken = this.typeMapFor(type).sinceToken, - adapter = this.adapterForType(type); - - set(array, 'isUpdating', true); - - if (adapter && adapter.findAll) { adapter.findAll(this, type, sinceToken); } - else { throw "Adapter is either null or does not implement `findAll` method"; } - }, - - /** - */ - sinceForType: function(type, sinceToken) { - this.typeMapFor(type).sinceToken = sinceToken; - }, - - /** - */ - 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. - - @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.registerRecordArray(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. - - @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, filterFunction: filter }); - - this.registerRecordArray(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) { - return !Ember.isNone(this.typeMapFor(type).idToCid[id]); - }, - - // ............ - // . 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; } - - var cidToData = this.clientIdToData, - clientId = reference.clientId, - data = cidToData[clientId]; - - if (typeof data === "object") { - this.updateRecordArrays(type, clientId); - } - }, - - // ................. - // . BASIC ADAPTER . - // ................. - - scheduleSave: function(record) { - this._recordsToSave.add(record); - Ember.run.once(this, 'flushSavedRecords'); - }, - - flushSavedRecords: function() { - var created = Ember.OrderedSet.create(); - var updated = Ember.OrderedSet.create(); - var deleted = Ember.OrderedSet.create(); - - this._recordsToSave.forEach(function(record) { - if (get(record, 'isNew')) { - created.add(record); - } else if (get(record, 'isDeleted')) { - deleted.add(record); - } else { - updated.add(record); - } - }); - - this._recordsToSave.clear(); - - get(this, '_adapter').commit(this, { - created: created, - updated: updated, - deleted: deleted - }); - }, - - // .............. - // . PERSISTING . - // .............. - - /** - This method delegates committing 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. - */ - commit: function() { - get(this, 'defaultTransaction').commit(); - }, - - /** - 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) { - record.adapterDidCommit(); - - if (data) { - this.updateId(record, data); - this.updateRecordData(record, data); - } else { - this.didUpdateAttributes(record); - } - }, - - /** - 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 relationship = this.relationshipChangeFor(get(record, '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) { - var clientId = get(record, 'clientId'), - cidToData = this.clientIdToData; - - cidToData[clientId] = 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 typeMap = this.typeMapFor(record.constructor), - clientId = get(record, 'clientId'), - oldId = get(record, 'id'), - type = record.constructor, - 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.idToCid[id] = clientId; - this.clientIdToId[clientId] = id; - this.referenceForClientId(clientId).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); - }, - - // ................. - // . RECORD ARRAYS . - // ................. - - /** - @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 - */ - registerRecordArray: function(array, type, filter) { - var recordArrays = this.typeMapFor(type).recordArrays; - - recordArrays.push(array); - - this.updateRecordArrayFilter(array, type, filter); - }, - - /** - @private - - Create a `DS.ManyArray` for a type and list of clientIds - and index the `ManyArray` under each clientId. This allows - us to efficiently remove records from `ManyArray`s when - they are deleted. - - @param {Class} type - @param {Array} clientIds - - @return {DS.ManyArray} - */ - createManyArray: function(type, clientIds) { - var array = DS.ManyArray.create({ type: type, content: clientIds, store: this }); - - clientIds.forEach(function(clientId) { - var recordArrays = this.recordArraysForClientId(clientId); - recordArrays.add(array); - }, this); - - return array; - }, - - /** - @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. - */ - updateRecordArrayFilter: function(array, type, filter) { - var typeMap = this.typeMapFor(type), - cidToData = this.clientIdToData, - clientIds = typeMap.clientIds, - clientId, data, shouldFilter, record; - - for (var i=0, l=clientIds.length; i<l; i++) { - clientId = clientIds[i]; - shouldFilter = false; - - data = cidToData[clientId]; - - if (typeof data === 'object') { - if (record = this.recordCache[clientId]) { - if (!get(record, 'isDeleted')) { shouldFilter = true; } - } else { - shouldFilter = true; - } - - if (shouldFilter) { - this.updateRecordArray(array, filter, type, clientId); - } - } - } - }, - - updateRecordArraysLater: function(type, clientId) { - Ember.run.once(this, function() { - this.updateRecordArrays(type, clientId); - }); - }, - - /** - @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(type, clientId) { - var recordArrays = this.typeMapFor(type).recordArrays, - filter; - - recordArrays.forEach(function(array) { - filter = get(array, 'filterFunction'); - this.updateRecordArray(array, filter, type, clientId); - }, this); - - // loop through all manyArrays containing an unloaded copy of this - // clientId and notify them that the record was loaded. - var manyArrays = this.loadingRecordArrays[clientId]; - - if (manyArrays) { - for (var i=0, l=manyArrays.length; i<l; i++) { - manyArrays[i].loadedRecord(); - } - - this.loadingRecordArrays[clientId] = null; - } - }, - - /** - @private - - Update an individual filter. - - @param {DS.FilteredRecordArray} array - @param {Function} filter - @param {Class} type - @param {Number|String} clientId - */ - updateRecordArray: function(array, filter, type, clientId) { - var shouldBeInArray, record; - - if (!filter) { - shouldBeInArray = true; - } else { - record = this.findByClientId(type, clientId); - shouldBeInArray = filter(record); - } - - var content = get(array, 'content'); - - var recordArrays = this.recordArraysForClientId(clientId); - var reference = this.referenceForClientId(clientId); - - 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 - */ - removeFromRecordArrays: function(record) { - var reference = get(record, '_reference'); - var recordArrays = this.recordArraysForClientId(reference.clientId); - - recordArrays.forEach(function(array) { - array.removeReference(reference); - }); - }, - - // ............ - // . INDEXING . - // ............ - - /** - @private - - Return a list of all `DS.RecordArray`s a clientId is - part of. - - @return {Object(clientId: Ember.OrderedSet)} - */ - recordArraysForClientId: function(clientId) { - var recordArrays = get(this, 'recordArraysByClientId'); - var ret = recordArrays[clientId]; - - if (!ret) { - ret = recordArrays[clientId] = Ember.OrderedSet.create(); - } - - return ret; - }, - - typeMapFor: function(type) { - var typeMaps = get(this, 'typeMaps'); - var guidForType = Ember.guidFor(type); - - var typeMap = typeMaps[guidForType]; - - if (typeMap) { - return typeMap; - } else { - return (typeMaps[guidForType] = - { - idToCid: {}, - clientIds: [], - recordArrays: [] - }); - } - }, - - /** @private - - For a given type and id combination, returns the client id used by the store. - If no client id has been assigned yet, one will be created and returned. - - @param {DS.Model} type - @param {String|Number} id - */ - clientIdForId: function(type, id) { - id = coerceId(id); - - var clientId = this.typeMapFor(type).idToCid[id]; - if (clientId !== undefined) { return clientId; } - - return this.pushData(UNLOADED, id, type); - }, - - /** - @private - - This method works exactly like `clientIdForId`, but does not - require looking up the `typeMap` for every `clientId` and - invoking a method per `clientId`. - */ - clientIdsForIds: function(type, ids) { - var typeMap = this.typeMapFor(type), - idToClientIdMap = typeMap.idToCid; - - return map(ids, function(id) { - id = coerceId(id); - - var clientId = idToClientIdMap[id]; - if (clientId) { return clientId; } - return this.pushData(UNLOADED, id, type); - }, this); - }, - - typeForClientId: function(clientId) { - return this.clientIdToType[clientId]; - }, - - idForClientId: function(clientId) { - return this.clientIdToId[clientId]; - }, - - // ................ - // . 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 typeMap = this.typeMapFor(type), - cidToData = this.clientIdToData, - clientId = typeMap.idToCid[id], - cidToPrematerialized = this.clientIdToPrematerializedData; - - if (clientId !== undefined) { - cidToData[clientId] = data; - cidToPrematerialized[clientId] = prematerialized; - - var record = this.recordCache[clientId]; - if (record) { - once(record, 'loadedData'); - } - } else { - clientId = this.pushData(data, id, type); - cidToPrematerialized[clientId] = prematerialized; - } - - this.updateRecordArraysLater(type, clientId); - - return this.referenceForClientId(clientId); - }, - - prematerialize: function(reference, prematerialized) { - this.clientIdToPrematerializedData[reference.clientId] = prematerialized; - }, - - 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) { - record.materializeHasMany(key, ids); - - // 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 - - Stores data for the specified type and id combination and returns - the client id. - - @param {Object} data - @param {String|Number} id - @param {DS.Model} type - @returns {Number} - */ - pushData: function(data, id, type) { - var typeMap = this.typeMapFor(type); - - var idToClientIdMap = typeMap.idToCid, - clientIdToIdMap = this.clientIdToId, - clientIdToTypeMap = this.clientIdToType, - clientIds = typeMap.clientIds, - cidToData = this.clientIdToData; - - Ember.assert('The id ' + id + ' has already been used with another record of type ' + type.toString() + '.', !id || !idToClientIdMap[id]); - - var clientId = ++this.clientIdCounter; - - cidToData[clientId] = data; - clientIdToTypeMap[clientId] = type; - - // if we're creating an item, this process will be done - // later, once the object has been persisted. - if (id) { - idToClientIdMap[id] = clientId; - clientIdToIdMap[clientId] = id; - } - - clientIds.push(clientId); - - return clientId; - }, - - // .......................... - // . RECORD MATERIALIZATION . - // .......................... - - materializeRecord: function(type, clientId, id) { - var record; - - this.recordCache[clientId] = record = type._create({ - store: this, - clientId: clientId - }); - - set(record, 'id', id); - - get(this, 'defaultTransaction').adoptRecord(record); - - record.loadingData(); - return record; - }, - - dematerializeRecord: function(record) { - var id = get(record, 'id'), - clientId = get(record, 'clientId'), - type = this.typeForClientId(clientId), - typeMap = this.typeMapFor(type); - - record.updateRecordArrays(); - - delete this.recordCache[clientId]; - delete this.clientIdToId[clientId]; - delete this.clientIdToType[clientId]; - delete this.clientIdToData[clientId]; - delete this.recordArraysByClientId[clientId]; - - if (id) { delete typeMap.idToCid[id]; } - }, - - 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(clientId, childKey, parentClientId, parentKey, type) { - 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 = this.recordForReference(reference), - 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() { -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. -*/ - -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.attributeName] = change; -}; - -var didSetProperty = function(manager, context) { - var change = get(manager, 'record')._changesToSync[context.attributeName]; - change.value = get(get(manager, 'record'), context.name); - change.sync(); -}; - -DS.State = Ember.State.extend({ - 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({ - // TRANSITIONS - enter: function(manager) { - var dirtyType = get(this, 'dirtyType'), - record = get(manager, 'record'); - - record.withTransaction(function (t) { - t.recordBecameDirty(dirtyType, record); - }); - }, - - // EVENTS - willSetProperty: willSetProperty, - didSetProperty: didSetProperty, - - becomeDirty: Ember.K, - - willCommit: function(manager) { - manager.transitionTo('inFlight'); - }, - - becameClean: function(manager) { - var record = get(manager, 'record'), - dirtyType = get(this, 'dirtyType'); - - record.withTransaction(function(t) { - t.recordBecameClean(dirtyType, record); - }); - - manager.transitionTo('loaded.materializing'); - }, - - becameInvalid: function(manager) { - var dirtyType = get(this, 'dirtyType'), - record = get(manager, 'record'); - - record.withTransaction(function (t) { - t.recordBecameInFlight(dirtyType, record); - }); - - 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 dirtyType = get(this, 'dirtyType'), - record = get(manager, 'record'); - - record.becameInFlight(); - - record.withTransaction(function (t) { - t.recordBecameInFlight(dirtyType, record); - }); - }, - - // EVENTS - didCommit: function(manager) { - var dirtyType = get(this, 'dirtyType'), - record = get(manager, 'record'); - - record.withTransaction(function(t) { - t.recordBecameClean('inflight', record); - }); - - manager.transitionTo('saved'); - manager.send('invokeLifecycleCallbacks', dirtyType); - }, - - 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.recordBecameClean('inflight', 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.withTransaction(function(t) { - t.recordIsMoving('created', 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'); - - record.withTransaction(function(t) { - t.recordIsMoving('updated', record); - }); - - manager.transitionTo('deleted'); - get(manager, 'record').clearRelationships(); - } -}); - -var states = { - rootState: Ember.State.create({ - // FLAGS - 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({ - // EVENTS - loadedData: didChangeData, - - materializingData: function(manager) { - manager.transitionTo('loaded.materializing.firstTime'); - } - }), - - // 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({ - // FLAGS - isLoaded: false, - - // EVENTS - willSetProperty: Ember.K, - didSetProperty: Ember.K, - - didChangeData: didChangeData, - - finishedMaterializing: function(manager) { - manager.transitionTo('loaded.saved'); - }, - - // SUBSTATES - firstTime: DS.State.create({ - 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) { - manager.transitionTo('deleted.saved'); - get(manager, 'record').clearRelationships(); - }, - - invokeLifecycleCallbacks: function(manager, dirtyType) { - var record = get(manager, 'record'); - if (dirtyType === 'created') { - record.trigger('didCreate', record); - } else { - record.trigger('didUpdate', 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.removeFromRecordArrays(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({ - // TRANSITIONS - enter: function(manager) { - var record = get(manager, 'record'); - - record.withTransaction(function(t) { - t.recordBecameDirty('deleted', record); - }); - }, - - // 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.recordBecameClean('deleted', 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(); - - record.withTransaction(function (t) { - t.recordBecameInFlight('deleted', record); - }); - }, - - // EVENTS - didCommit: function(manager) { - var record = get(manager, 'record'); - - record.withTransaction(function(t) { - t.recordBecameClean('inflight', 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); - } - }) - }), - - // 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(); - -DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, { - isLoaded: retrieveFromCurrentState, - isReloading: retrieveFromCurrentState, - isDirty: retrieveFromCurrentState, - isSaving: retrieveFromCurrentState, - isDeleted: retrieveFromCurrentState, - isError: retrieveFromCurrentState, - isNew: retrieveFromCurrentState, - isValid: 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. - - Available options: - - * `includeId`: `true` if the record's ID should be included in the - JSON representation. - - @param {Object} options - @returns {Object} an object whose values are primitive JSON values only - */ - serialize: function(options) { - var store = get(this, 'store'); - return store.serialize(this, options); - }, - - toJSON: function() { - var serializer = DS.JSONSerializer.create(); - return serializer.serialize(this); - }, - - didLoad: Ember.K, - didReload: Ember.K, - didUpdate: Ember.K, - didCreate: Ember.K, - didDelete: Ember.K, - becameInvalid: Ember.K, - becameError: Ember.K, - - data: Ember.computed(function() { - if (!this._data) { - this.materializeData(); - } - - 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._relationshipChanges = {}; - 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'); - }, - - setProperty: function(key, value, oldValue) { - this.send('setProperty', { key: key, value: value, oldValue: oldValue }); - }, - - /** - 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`). - */ - reload: function() { - this.send('reloadRecord'); - }, - - 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') { - get(this, name).clear(); - } - }, 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() { - var relationships = get(this.constructor, 'relationshipsByName'); - - this.updateRecordArraysLater(); - - relationships.forEach(function(name, relationship) { - if (relationship.kind === 'hasMany') { - this.hasManyDidChange(relationship.key); - } - }, this); - - this.send('finishedMaterializing'); - }, 'data'), - - 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 it was already a reference, return the reference - if (typeof id === 'object') { return id; } - return store.referenceForId(type, id); - }); - - set(cachedValue, 'content', Ember.A(references)); - } - }, - - updateRecordArraysLater: function() { - Ember.run.once(this, this.updateRecordArrays); - }, - - setupData: function(prematerialized) { - 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, ids) { - this._data.hasMany[name] = ids; - }, - - materializeBelongsTo: function(name, id) { - this._data.belongsTo[name] = 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() { - }, - - // FOR USE BY THE BASIC ADAPTER - - save: function() { - this.get('store').scheduleSave(this); - }, - - // 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(); - }, - - _reference: Ember.computed(function() { - return get(this, 'store').referenceForClientId(get(this, 'clientId')); - }), - - 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({ - isLoaded: storeAlias('recordIsLoaded'), - find: storeAlias('find'), - all: storeAlias('all'), - query: storeAlias('findQuery'), - filter: storeAlias('filter'), - - _create: DS.Model.create, - - 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."); - }, - - createRecord: storeAlias('createRecord') -}); - -})(); - - - -(function() { -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; - }) -}); - -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(); - }, - - destroy: function() { - delete this.store.recordForReference(this.reference)._changesToSync[this.name]; - } -}; - -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) { - 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() { - -})(); - - - -(function() { -var get = Ember.get, set = Ember.set, - none = 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 (arguments.length === 2) { - return value === undefined ? null : value; - } - - var data = get(this, 'data').belongsTo, - store = get(this, 'store'), id; - - if (typeof type === 'string') { - type = get(this, type, false) || get(Ember.lookup, type); - } - - id = data[key]; - - if(!id) { - return null; - } else if (typeof id === 'object') { - return store.recordForReference(id); - } else { - return store.find(type, 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; -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 = data[key]; - relationship = store.findMany(type, ids, this, meta); - set(relationship, 'owner', this); - set(relationship, 'name', key); - - 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() { -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; - }, - - /** - 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'); - associatons.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); - } - - if (!types.contains(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(), type; - - 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); - } -}); - -/** - @private - - Helper method to look up the name of the inverse of a relationship. - - In a has-many relationship, there are always two sides: the `belongsTo` side - and the `hasMany` side. When one side changes, the other side should be updated - automatically. - - Given a model, the model of the inverse, and the kind of the relationship, this - helper returns the name of the relationship on the inverse. - - For example, imagine the following two associated models: - - App.Post = DS.Model.extend({ - comments: DS.hasMany('App.Comment') - }); - - App.Comment = DS.Model.extend({ - post: DS.belongsTo('App.Post') - }); - - If the `post` property of a `Comment` was modified, Ember Data would invoke - this helper like this: - - DS._inverseNameFor(App.Comment, App.Post, 'hasMany'); - //=> 'comments' - - Ember Data uses the name of the relationship returned to reflect the changed - relationship on the other side. -*/ -DS._inverseRelationshipFor = function(modelType, inverseModelType) { - var relationshipMap = get(modelType, 'relationships'), - possibleRelationships = relationshipMap.get(inverseModelType), - possible, actual, oldValue; - - if (!possibleRelationships) { return; } - if (possibleRelationships.length > 1) { return; } - return possibleRelationships[0]; -}; - -/** - @private - - Given a model and a relationship name, returns the model type of - the named relationship. - - App.Post = DS.Model.extend({ - comments: DS.hasMany('App.Comment') - }); - - DS._inverseTypeFor(App.Post, 'comments'); - //=> App.Comment - @param {DS.Model class} modelType - @param {String} relationshipName - @return {DS.Model class} -*/ -DS._inverseTypeFor = function(modelType, relationshipName) { - var relationships = get(modelType, 'relationshipsByName'), - relationship = relationships.get(relationshipName); - - if (relationship) { return relationship.type; } -}; - -})(); - - - -(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.store = options.store; - this.committed = {}; - this.changeType = options.changeType; -}; - -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, type, otherContainerType,assoc; - var knownContainerType = knownSide.kind; - var options = recordType.metaForProperty(knownKey).options; - var otherType = DS._inverseTypeFor(recordType, knownKey); - - if(options.inverse){ - key = options.inverse; - otherContainerType = get(otherType, 'relationshipsByName').get(key).kind; - } - else if(assoc = DS._inverseRelationshipFor(otherType, recordType)){ - key = assoc.name; - otherContainerType = assoc.kind; - } - if(!key){ - return knownContainerType === "belongsTo" ? "oneToNone" : "manyToNone"; - } - else{ - if(otherContainerType === "belongsTo"){ - return knownContainerType === "belongsTo" ? "oneToOne" : "manyToOne"; - } - else{ - return knownContainerType === "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, key, 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) { - // Get the type of the child based on the child's client ID - var childType = childReference.type, 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. - 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) { - // Get the type of the child based on the child's client ID - var childType = childReference.type, 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 = inverseBelongsToName(options.parentType, childType, options.key); - //DS.OneToOneChange.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: "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) { - // Get the type of the child based on the child's client ID - var childType = childReference.type, 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 = inverseBelongsToName(options.parentType, childType, options.key); - 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, null, change); - - - return change; -}; - - -DS.OneToManyChange.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.OneToManyChange.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(); - } - } -}; - -DS.OneToManyChange.ensureSameTransaction = function(changes, store){ - var records = Ember.A(); - forEach(changes, function(change){ - records.addObject(change.getSecondRecord()); - records.addObject(change.getFirstRecord()); - }); - var transaction = store.ensureSameTransaction(records); - forEach(changes, function(change){ - change.transaction = transaction; - }); -}; - -DS.RelationshipChange.prototype = { - - getSecondRecordName: function() { - var name = this.secondRecordName, store = this.store, parent; - - if (!name) { - parent = this.secondRecordReference; - if (!parent) { return; } - - var childType = this.firstRecordReference.type; - var inverseType = DS._inverseTypeFor(childType, this.firstRecordName); - name = inverseHasManyName(inverseType, childType, this.firstRecordName); - this.secondRecordName = name; - } - - return name; - }, - - /** - Get the name of the relationship on the belongsTo side. - - @returns {String} - */ - getFirstRecordName: function() { - var name = this.firstRecordName, store = this.store, parent, child; - - if (!name) { - parent = this.secondRecordReference; - child = this.firstRecordReference; - if (!(child && parent)) { return; } - - name = DS._inverseRelationshipFor(child.type, parent.type).name; - - this.firstRecordName = name; - } - - return name; - }, - - /** @private */ - destroy: function() { - var childReference = this.childReference, - belongsToName = this.getFirstRecordName(), - hasManyName = this.getSecondRecordName(), - store = this.store, - child, oldParent, newParent, lastParent, transaction; - - store.removeRelationshipChangeFor(childReference, belongsToName, this.parentReference, hasManyName, this.changeType); - - if (transaction = this.transaction) { - transaction.relationshipBecameClean(this); - } - }, - - /** @private */ - getByReference: function(reference) { - var store = this.store; - - // return null or undefined if the original reference was null or undefined - if (!reference) { return reference; } - - if (store.recordIsMaterialized(reference)) { - return store.recordForReference(reference); - } - }, - - 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 = this.store.ensureSameTransaction([child, parentRecord]); - - this.transaction = transaction; - return transaction; - }, - - callChangeEvents: function(){ - var hasManyName = this.getSecondRecordName(), - belongsToName = this.getFirstRecordName(), - 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); - - var transaction = this.ensureSameTransaction(); - transaction.relationshipBecameDirty(this); - - 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.firstdRecordKind === "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); - - var transaction = this.ensureSameTransaction(firstRecord, secondRecord, secondRecordName, firstRecordName); - transaction.relationshipBecameDirty(this); - - 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.firstdRecordKind === "hasMany"){ - firstRecord.suspendRelationshipObservers(function(){ - get(firstRecord, firstRecordName).removeObject(secondRecord); - }); - } - } - - this.coalesce(); -}; - -function inverseBelongsToName(parentType, childType, hasManyName) { - // Get the options passed to the parent's DS.hasMany() - var options = parentType.metaForProperty(hasManyName).options; - var belongsToName; - - if (belongsToName = options.inverse) { - return belongsToName; - } - - return DS._inverseRelationshipFor(childType, parentType).name; -} - -function inverseHasManyName(parentType, childType, belongsToName) { - var options = childType.metaForProperty(belongsToName).options; - var hasManyName; - - if (hasManyName = options.inverse) { - return hasManyName; - } - - return DS._inverseRelationshipFor(parentType, childType).name; -} - -})(); - - - -(function() { - -})(); - - - -(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) { - if (Application.registerInjection) { - Application.registerInjection({ - name: "store", - before: "controllers", - - // If a store subclass is defined, like App.Store, - // instantiate it and inject it into the router. - injection: function(app, stateManager, property) { - if (!stateManager) { return; } - if (property === 'Store') { - set(stateManager, 'store', app[property].create()); - } - } - }); - - Application.registerInjection({ - name: "giveStoreToControllers", - after: ['store','controllers'], - - // For each controller, set its `store` property - // to the DS.Store instance we created above. - injection: function(app, stateManager, property) { - if (!stateManager) { return; } - if (/^[A-Z].*Controller$/.test(property)) { - var controllerName = property.charAt(0).toLowerCase() + property.substr(1); - var store = stateManager.get('store'); - var controller = stateManager.get(controllerName); - if(!controller) { return; } - - controller.set('store', store); - } - } - }); - } else if (Application.initializer) { - 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() { -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. -*/ -DS.Serializer = Ember.Object.extend({ - init: function() { - this.mappings = Ember.Map.create(); - this.configurations = Ember.Map.create(); - this.globalConfigurations = {}; - }, - - extract: mustImplement('extract'), - extractMany: mustImplement('extractMany'), - - extractRecordRepresentation: function(loader, type, json, shouldSideload) { - var mapping = this.mappingForType(type); - var embeddedData, prematerialized = {}, reference; - - if (shouldSideload) { - reference = loader.sideload(type, json); - } else { - reference = loader.load(type, json); - } - - this.eachEmbeddedHasMany(type, function(name, relationship) { - var embeddedData = json[this.keyFor(relationship)]; - if (!isNone(embeddedData)) { - this.extractEmbeddedHasMany(loader, relationship, embeddedData, reference, prematerialized); - } - }, this); - - this.eachEmbeddedBelongsTo(type, function(name, relationship) { - var embeddedData = json[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 reference = this.extractRecordRepresentation(loader, relationship.type, 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; - } - - return reference; - }, this); - - prematerialized[relationship.key] = references; - }, - - extractEmbeddedBelongsTo: function(loader, relationship, data, parent, prematerialized) { - var reference = this.extractRecordRepresentation(loader, relationship.type, 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; - } - }, - - //....................... - //. 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. - * 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`. - - @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); - } - } - - 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. - - @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 tranforms, you should rarely need to override this hook. - - @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. - - @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: Ember.K, - - /** - 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) - - @param {any} data the serialized representation that is being built - @param {String} key the resolved primary key - @param {id} id the serialized id - */ - addId: Ember.K, - - /** - A hook you can use to change how relationships are added to the serialized - representation of a record. - - By default, `addAttributes` 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. - - @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. - - @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: Ember.K, - - /** - 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. - - @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: Ember.K, - - /** - 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(); - } - }); - ``` - - @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"; - } - }); - ``` - - @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(); - } - }); - ``` - - @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(); - } - }); - ``` - - @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 a 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, hash, prematerialized) { - record.eachRelationship(function(name, relationship) { - if (relationship.kind === 'hasMany') { - if (prematerialized && prematerialized.hasOwnProperty(name)) { - record.materializeHasMany(name, prematerialized[name]); - } else { - this.materializeHasMany(name, record, hash, relationship, prematerialized); - } - } else if (relationship.kind === 'belongsTo') { - if (prematerialized && prematerialized.hasOwnProperty(name)) { - record.materializeBelongsTo(name, prematerialized[name]); - } else { - this.materializeBelongsTo(name, record, hash, relationship, prematerialized); - } - } - }, this); - }, - - materializeHasMany: function(name, record, hash, relationship) { - var key = this._keyForHasMany(record.constructor, relationship.key); - record.materializeHasMany(name, this.extractHasMany(record.constructor, hash, key)); - }, - - materializeBelongsTo: function(name, record, hash, relationship) { - var key = this._keyForBelongsTo(record.constructor, relationship.key); - record.materializeBelongsTo(name, this.extractBelongsTo(record.constructor, hash, key)); - }, - - _extractEmbeddedRelationship: function(type, hash, name, relationshipType) { - var key = this['_keyFor' + relationshipType](type, name); - - if (this.embeddedType(type, name)) { - return this['extractEmbedded' + relationshipType](type, hash, key); - } - }, - - _extractEmbeddedBelongsTo: function(type, hash, name) { - return this._extractEmbeddedRelationship(type, hash, name, 'BelongsTo'); - }, - - _extractEmbeddedHasMany: function(type, hash, name) { - return this._extractEmbeddedRelationship(type, hash, name, 'HasMany'); - }, - - /** - @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. - - @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. - - @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. - - @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`. - - @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`. - - @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`. - - @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. - - @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. - - @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. - - @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(deserialized) { - return Ember.A(objects).objectAt(deserialized); - }, - serialize: function(serialized) { - return Ember.EnumerableUtils.indexOf(objects, serialized); - }, - 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 = Ember.create(this.globalConfigurations); - Ember.merge(config, configuration); - - this.configurations.set(type, config); - }, - - mappingForType: function(type) { - this._reifyMappings(); - return this.mappings.get(type) || {}; - }, - - configurationForType: function(type) { - this._reifyConfigurations(); - return this.configurations.get(type) || this.globalConfigurations; - }, - - _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) { - var type = record.constructor; - - 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) { - var type = record.constructor; - - 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); - } -}); - - -})(); - - - -(function() { -var none = Ember.isNone; - -/** - DS.Transforms is a hash of transforms used by DS.Serializer. -*/ -DS.JSONTransforms = { - string: { - deserialize: function(serialized) { - return none(serialized) ? null : String(serialized); - }, - - serialize: function(deserialized) { - return none(deserialized) ? null : String(deserialized); - } - }, - - number: { - deserialize: function(serialized) { - return none(serialized) ? null : Number(serialized); - }, - - serialize: function(deserialized) { - return none(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; - var date = null; - - if (type === "string" || type === "number") { - // this is a fix for Safari 5.1.5 on Mac which does not accept timestamps as yyyy-mm-dd - if (type === "string" && serialized.search(/^\d{4}-\d{2}-\d{2}$/) !== -1) { - serialized += "T00:00:00Z"; - } - - date = new Date(serialized); - - // this is a fix for IE8 which does not accept timestamps in ISO 8601 format - if (type === "string" && isNaN(date)) { - date = new Date(Date.parse(serialized.replace(/\-/ig, '/').replace(/Z$/, '').split('.')[0])); - } - - return date; - } 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 if (date === undefined) { - return undefined; - } else { - return null; - } - } - } -}; - -})(); - - - -(function() { -var get = Ember.get, set = Ember.set; - -var generatedId = 0; - -DS.JSONSerializer = DS.Serializer.extend({ - init: function() { - this._super(); - - if (!get(this, 'transforms')) { - this.set('transforms', DS.JSONTransforms); - } - - this.sideloadMapping = Ember.Map.create(); - - this.configure({ - meta: 'meta', - since: 'since' - }); - }, - - configure: function(type, configuration) { - if (type && !configuration) { - return this._super(type); - } - - var sideloadAs = configuration.sideloadAs; - - if (sideloadAs) { - this.sideloadMapping.set(sideloadAs, type); - - // Set a flag indicating that mappings may need to be normalized - // (i.e. converted from strings -> types) before sideloading. - // We can't do this conversion immediately here, because `configure` - // may be called before certain types have been defined. - this.sideloadMapping.normalized = false; - - 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; - }, - - /** - @private - - Creates an empty hash that will be filled in by the hooks called from the - `serialize()` method. - - @return {Object} - */ - createSerializedForm: function() { - return {}; - }, - - 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; - } - }, - - extractHasMany: function(type, hash, key) { - return hash[key]; - }, - - extractBelongsTo: function(type, hash, key) { - return hash[key]; - }, - - addBelongsTo: function(hash, record, key, relationship) { - var type = record.constructor, - name = relationship.key, - value = null, - embeddedChild; - - if (this.embeddedType(type, name)) { - if (embeddedChild = get(record, name)) { - value = this.serialize(embeddedChild, { includeId: true }); - } - - hash[key] = value; - } else { - var id = get(record, relationship.key+'.id'); - if (!Ember.isNone(id)) { hash[key] = id; } - } - }, - - /** - 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 = [], - 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 })); - }, this); - - // Set the appropriate property of the serialized JSON to the - // array of serialized embedded records - hash[key] = serializedHasMany; - }, - - // 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]); - } - }, - - 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 = json[this.configOption(type, 'meta')], since; - if (!meta) { return; } - - if (since = meta[this.configOption(type, 'since')]) { - loader.sinceForType(type, since); - } - }, - - /** - @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.normalizeSideloadMappings(); - this.configureSideloadMappingForType(type); - - for (var prop in json) { - if (!json.hasOwnProperty(prop) || - prop === root || - prop === this.configOption(type, 'meta')) { - continue; - } - - sideloadedType = this.sideloadMapping.get(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 - - Iterates over all the `sideloadAs` mappings and converts any that are - strings to their equivalent types. - - This is an optimization used to avoid performing lookups for every - call to `sideload`. - */ - normalizeSideloadMappings: function() { - if (! this.sideloadMapping.normalized) { - this.sideloadMapping.forEach(function(key, value) { - if (typeof value === 'string') { - this.sideloadMapping.set(key, get(Ember.lookup, value)); - } - }, this); - this.sideloadMapping.normalized = true; - } - }, - - /** - @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.sideloadMappingForType(relatedType); - if (!root) { - root = this.defaultSideloadRootForType(relatedType); - this.sideloadMapping.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); - } - }, - - // 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; - } - }, - - /** - @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 - @returns {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 - - Determines the root name mapped to a particular sideloaded type. - - @param {DS.Model subclass} type - @returns {String} name of the root element, if any is registered - */ - sideloadMappingForType: function(type) { - this.sideloadMapping.forEach(function(key, value) { - if (type === value) { - return key; - } - }); - }, - - /** - @private - - The default root name for a particular sideloaded type. - - @param {DS.Model subclass} type - @returns {String} name of the root element - */ - defaultSideloadRootForType: function(type) { - return this.pluralize(this.rootForType(type)); - } -}); - -})(); - - - -(function() { -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.load(type, data); - }, - - sideloadMany: function(type, array) { - return store.loadMany(type, array); - }, - - prematerialize: function(reference, prematerialized) { - store.prematerialize(reference, prematerialized); - }, - - sinceForType: function(type, since) { - store.sinceForType(type, since); - } - }; -} - -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({ - revision: 3, - 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()` -*/ - -var get = Ember.get, set = Ember.set, merge = Ember.merge; - -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: - - ```javascript - 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`. - - @param {DS.Store} store - @param {subclass of DS.Model} type - @param {any} payload - */ - load: function(store, type, payload) { - var loader = loaderFor(store); - 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. - - @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. - - @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. - - @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'), - mappings = serializer.mappingForType(type); - - 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. - - @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. - - @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. - - @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. - - @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. - - @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. - - @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. - - @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. - - @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. - - @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. - - @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. - - @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; - - for (prop in transforms) { - if (!transforms.hasOwnProperty(prop) || prop in seen) { continue; } - seen[prop] = true; - - serializer.registerTransform(prop, transforms[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. - - @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); - }); - } - */ - 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} } - - @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; - } - */ - 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; - }, - - 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, mappingsKey) { - merge(newValue, oldValue); - - return newValue; - } -}); - -})(); - - - -(function() { -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; - }, - - /** - @private - - Creates an empty hash that will be filled in by the hooks called from the - `serialize()` method. - - @return {Object} - */ - createSerializedForm: function() { - return {}; - }, - - 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) { - return hash[key]; - } -}); - -})(); - - - -(function() { -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`. - -*/ -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){ - if(!fixture.id){ - throw new Error(fmt('the id property must be defined 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 id = this.extractId(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) { - var adapter = this; - - 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() { -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"; - } -}); - -})(); - - - -(function() { -/*global jQuery*/ - -var get = Ember.get, set = Ember.set, merge = Ember.merge; - -/** - 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" - } - } - ``` -*/ -DS.RESTAdapter = DS.Adapter.extend({ - 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; - }, - - createRecord: function(store, type, record) { - var root = this.rootForType(type); - - var data = {}; - data[root] = this.serialize(record, { includeId: true }); - - this.ajax(this.buildURL(root), "POST", { - data: data, - context: this, - success: function(json) { - Ember.run(this, function(){ - this.didCreateRecord(store, type, record, json); - }); - }, - error: function(xhr) { - this.didError(store, type, record, xhr); - } - }); - }, - - 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); - } - }, - - createRecords: function(store, type, records) { - 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); - - this.ajax(this.buildURL(root), "POST", { - data: data, - context: this, - success: function(json) { - Ember.run(this, function(){ - this.didCreateRecords(store, type, records, json); - }); - } - }); - }, - - updateRecord: function(store, type, record) { - var id = get(record, 'id'); - var root = this.rootForType(type); - - var data = {}; - data[root] = this.serialize(record); - - this.ajax(this.buildURL(root, id), "PUT", { - data: data, - context: this, - success: function(json) { - Ember.run(this, function(){ - this.didSaveRecord(store, type, record, json); - }); - }, - error: function(xhr) { - this.didError(store, type, record, xhr); - } - }); - }, - - updateRecords: function(store, type, records) { - 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); - - this.ajax(this.buildURL(root, "bulk"), "PUT", { - data: data, - context: this, - success: function(json) { - Ember.run(this, function(){ - this.didSaveRecords(store, type, records, json); - }); - } - }); - }, - - deleteRecord: function(store, type, record) { - var id = get(record, 'id'); - var root = this.rootForType(type); - - this.ajax(this.buildURL(root, id), "DELETE", { - context: this, - success: function(json) { - Ember.run(this, function(){ - this.didSaveRecord(store, type, record, json); - }); - } - }); - }, - - deleteRecords: function(store, type, records) { - if (get(this, 'bulkCommit') === false) { - return this._super(store, type, records); - } - - var root = this.rootForType(type), - plural = this.pluralize(root), - serializer = get(this, 'serializer'); - - var data = {}; - data[plural] = []; - records.forEach(function(record) { - data[plural].push(serializer.serializeId( get(record, 'id') )); - }); - - this.ajax(this.buildURL(root, 'bulk'), "DELETE", { - data: data, - context: this, - success: function(json) { - Ember.run(this, function(){ - this.didSaveRecords(store, type, records, json); - }); - } - }); - }, - - find: function(store, type, id) { - var root = this.rootForType(type); - - this.ajax(this.buildURL(root, id), "GET", { - success: function(json) { - Ember.run(this, function(){ - this.didFindRecord(store, type, json, id); - }); - } - }); - }, - - findAll: function(store, type, since) { - var root = this.rootForType(type); - - this.ajax(this.buildURL(root), "GET", { - data: this.sinceQuery(since), - success: function(json) { - Ember.run(this, function(){ - this.didFindAll(store, type, json); - }); - } - }); - }, - - findQuery: function(store, type, query, recordArray) { - var root = this.rootForType(type); - - this.ajax(this.buildURL(root), "GET", { - data: query, - success: function(json) { - Ember.run(this, function(){ - this.didFindQuery(store, type, json, recordArray); - }); - } - }); - }, - - findMany: function(store, type, ids, owner) { - var root = this.rootForType(type); - ids = this.serializeIds(ids); - - this.ajax(this.buildURL(root), "GET", { - data: {ids: ids}, - success: function(json) { - Ember.run(this, function(){ - this.didFindMany(store, type, json); - }); - } - }); - }, - - /** - @private - - This method serializes a list of IDs using `serializeId` - - @returns {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 data = JSON.parse(xhr.responseText); - store.recordWasInvalid(record, data['errors']); - } else { - this._super.apply(this, arguments); - } - }, - - ajax: function(url, type, hash) { - hash.url = url; - hash.type = type; - hash.dataType = 'json'; - hash.contentType = 'application/json; charset=utf-8'; - hash.context = this; - - if (hash.data && type !== 'GET') { - hash.data = JSON.stringify(hash.data); - } - - jQuery.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 (this.namespace !== undefined) { - 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() { -var camelize = Ember.String.camelize, - capitalize = Ember.String.capitalize, - get = Ember.get, - map = Ember.ArrayPolyfills.map, - registeredTransforms; - -var passthruTransform = { - serialize: function(value) { return value; }, - deserialize: function(value) { return value; } -}; - -var defaultTransforms = { - string: passthruTransform, - boolean: passthruTransform, - number: passthruTransform -}; - -function camelizeKeys(json) { - var value; - - for (var prop in json) { - value = json[prop]; - delete json[prop]; - json[camelize(prop)] = value; - } -} - -function munge(json, callback) { - callback(json); -} - -function applyTransforms(json, type, transformType) { - var transforms = registeredTransforms[transformType]; - - Ember.assert("You are trying to apply the '" + transformType + "' transforms, but you didn't register any transforms with that name", transforms); - - get(type, 'attributes').forEach(function(name, attribute) { - var attributeType = attribute.type, - value = json[name]; - - var transform = transforms[attributeType] || defaultTransforms[attributeType]; - - Ember.assert("Your model specified the '" + attributeType + "' type for the '" + name + "' attribute, but no transform for that type was registered", transform); - - json[name] = transform.deserialize(value); - }); -} - -function ObjectProcessor(json, type, store) { - this.json = json; - this.type = type; - this.store = store; -} - -ObjectProcessor.prototype = { - camelizeKeys: function() { - camelizeKeys(this.json); - return this; - }, - - munge: function(callback) { - munge(this.json, callback); - return this; - }, - - applyTransforms: function(transformType) { - applyTransforms(this.json, this.type, transformType); - return this; - } -}; - -function LoadObjectProcessor() { - ObjectProcessor.apply(this, arguments); -} - -LoadObjectProcessor.prototype = Ember.create(ObjectProcessor.prototype); - -LoadObjectProcessor.prototype.load = function() { - this.store.load(this.type, {}, this.json); -}; - -function loadObjectProcessorFactory(store, type) { - return function(json) { - return new LoadObjectProcessor(json, type, store); - }; -} - -function ArrayProcessor(json, type, array, store) { - this.json = json; - this.type = type; - this.array = array; - this.store = store; -} - -ArrayProcessor.prototype = { - load: function() { - var store = this.store, - type = this.type; - - var references = this.json.map(function(object) { - return store.load(type, {}, object); - }); - - this.array.load(references); - }, - - camelizeKeys: function() { - this.json.forEach(camelizeKeys); - return this; - }, - - munge: function(callback) { - this.json.forEach(function(object) { - munge(object, callback); - }); - return this; - }, - - applyTransforms: function(transformType) { - var type = this.type; - - this.json.forEach(function(object) { - applyTransforms(object, type, transformType); - }); - - return this; - } -}; - -function arrayProcessorFactory(store, type, array) { - return function(json) { - return new ArrayProcessor(json, type, array, store); - }; -} - -var HasManyProcessor = function(json, store, record, relationship) { - this.json = json; - this.store = store; - this.record = record; - this.type = record.constructor; - this.relationship = relationship; -}; - -HasManyProcessor.prototype = Ember.create(ArrayProcessor.prototype); - -HasManyProcessor.prototype.load = function() { - var store = this.store; - var ids = map.call(this.json, function(obj) { return obj.id; }); - - store.loadMany(this.relationship.type, this.json); - store.loadHasMany(this.record, this.relationship.key, ids); -}; - -function hasManyProcessorFactory(store, record, relationship) { - return function(json) { - return new HasManyProcessor(json, store, record, relationship); - }; -} - -function CreateProcessor(record, store, type) { - this.record = record; - ObjectProcessor.call(this, record.toJSON(), type, store); -} - -CreateProcessor.prototype = Ember.create(ObjectProcessor.prototype); - -CreateProcessor.prototype.save = function() {}; - -function createProcessorFactory(store, type) { - return function(record) { - return new CreateProcessor(record, store, type); - }; -} - -DS.BasicAdapter = DS.Adapter.extend({ - find: function(store, type, id) { - var sync = type.sync; - - Ember.assert("You are trying to use the BasicAdapter to find id '" + id + "' of " + type + " but " + type + ".sync was not found", sync); - Ember.assert("The sync code on " + type + " does not implement find(), but you are trying to find id '" + id + "'.", sync.find); - - sync.find(id, loadObjectProcessorFactory(store, type)); - }, - - findQuery: function(store, type, query, recordArray) { - var sync = type.sync; - - Ember.assert("You are trying to use the BasicAdapter to query " + type + " but " + type + ".sync was not found", sync); - Ember.assert("The sync code on " + type + " does not implement query(), but you are trying to query " + type + ".", sync.query); - - sync.query(query, arrayProcessorFactory(store, type, recordArray)); - }, - - findHasMany: function(store, record, relationship, data) { - var name = capitalize(relationship.key), - sync = record.constructor.sync, - processor = hasManyProcessorFactory(store, record, relationship); - - var options = { - relationship: relationship.key, - data: data - }; - - if (sync['find'+name]) { - sync['find' + name](record, options, processor); - } else if (sync.findHasMany) { - sync.findHasMany(record, options, processor); - } else { - Ember.assert("You are trying to use the BasicAdapter to find the " + relationship.key + " has-many relationship, but " + record.constructor + ".sync did not implement findHasMany or find" + name + ".", false); - } - }, - - createRecord: function(store, type, record) { - var sync = type.sync; - - sync.createRecord(record, createProcessorFactory(store, type)); - } -}); - -DS.registerTransforms = function(kind, object) { - registeredTransforms[kind] = object; -}; - -DS.clearTransforms = function() { - registeredTransforms = {}; -}; - -DS.clearTransforms(); - -})(); - - - -(function() { - -})(); - - - -(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. - -})(); - |