diff options
author | Peter Kozak <spag@golwen.net> | 2013-06-22 10:22:48 +0200 |
---|---|---|
committer | Peter Kozak <spag@golwen.net> | 2013-06-22 10:22:48 +0200 |
commit | 08ecc91d89a2b6a83daee4a1ffda5520e38e53c6 (patch) | |
tree | 2f15799d3948eaa26abacef033c687b4362f602b /public/js/libs/ember-data.js | |
parent | d21bf5513aa28187deb6b6c1ba21635b97ff82a4 (diff) | |
parent | 4ce04ec03bc8f362d48ded43833645e7f454cb5c (diff) |
Merge branch 'develop' of github.com:amooma/GS5 into develop
Diffstat (limited to 'public/js/libs/ember-data.js')
-rw-r--r-- | public/js/libs/ember-data.js | 4832 |
1 files changed, 2641 insertions, 2191 deletions
diff --git a/public/js/libs/ember-data.js b/public/js/libs/ember-data.js index 00a6e25..798478b 100644 --- a/public/js/libs/ember-data.js +++ b/public/js/libs/ember-data.js @@ -1,10 +1,119 @@ -// Last commit: 5fd6d65 (2013-03-28 01:13:50 +0100) +// Version: v0.13-33-g8cf224d +// Last commit: 8cf224d (2013-06-19 00:23:32 -0400) (function() { -window.DS = Ember.Namespace.create({ - // this one goes past 11 - CURRENT_API_REVISION: 12 +var define, requireModule; + +(function() { + var registry = {}, seen = {}; + + define = function(name, deps, callback) { + registry[name] = { deps: deps, callback: callback }; + }; + + requireModule = function(name) { + if (seen[name]) { return seen[name]; } + seen[name] = {}; + + var mod, deps, callback, reified , exports; + + mod = registry[name]; + + if (!mod) { + throw new Error("Module '" + name + "' not found."); + } + + deps = mod.deps; + callback = mod.callback; + reified = []; + exports; + + for (var i=0, l=deps.length; i<l; i++) { + if (deps[i] === 'exports') { + reified.push(exports = {}); + } else { + reified.push(requireModule(deps[i])); + } + } + + var value = callback.apply(this, reified); + return seen[name] = exports || value; + }; +})(); +(function() { +/** + @module data + @main data +*/ + +/** + All Ember Data methods and functions are defined inside of this namespace. + + @class DS + @static +*/ + +window.DS = Ember.Namespace.create(); + +})(); + + + +(function() { +var set = Ember.set; + +/** + This code registers an injection for Ember.Application. + + If an Ember.js developer defines a subclass of DS.Store on their application, + this code will automatically instantiate it and make it available on the + router. + + Additionally, after an application's controllers have been injected, they will + each have the store made available to them. + + For example, imagine an Ember.js application with the following classes: + + App.Store = DS.Store.extend({ + adapter: 'App.MyCustomAdapter' + }); + + App.PostsController = Ember.ArrayController.extend({ + // ... + }); + + When the application is initialized, `App.Store` will automatically be + instantiated, and the instance of `App.PostsController` will have its `store` + property set to that instance. + + Note that this code will only be run if the `ember-application` package is + loaded. If Ember Data is being used in an environment other than a + typical application (e.g., node.js where only `ember-runtime` is available), + this code will be ignored. +*/ + +Ember.onLoad('Ember.Application', function(Application) { + Application.initializer({ + name: "store", + + initialize: function(container, application) { + application.register('store:main', application.Store); + + // Eagerly generate the store so defaultStore is populated. + // TODO: Do this in a finisher hook + container.lookup('store:main'); + } + }); + + Application.initializer({ + name: "injectStore", + + initialize: function(container, application) { + application.inject('controller', 'store', 'store:main'); + application.inject('route', 'store', 'store:main'); + } + }); }); })(); @@ -12,16 +121,79 @@ window.DS = Ember.Namespace.create({ (function() { -var DeferredMixin = Ember.DeferredMixin, // ember-runtime/mixins/deferred - Evented = Ember.Evented, // ember-runtime/mixins/evented +/** + * Date.parse with progressive enhancement for ISO 8601 <https://github.com/csnover/js-iso8601> + * © 2011 Colin Snover <http://zetafleet.com> + * Released under MIT license. + */ + +Ember.Date = Ember.Date || {}; + +var origParse = Date.parse, numericKeys = [ 1, 4, 5, 6, 7, 10, 11 ]; +Ember.Date.parse = function (date) { + var timestamp, struct, minutesOffset = 0; + + // ES5 §15.9.4.2 states that the string should attempt to be parsed as a Date Time String Format string + // before falling back to any implementation-specific date parsing, so that’s what we do, even if native + // implementations could be faster + // 1 YYYY 2 MM 3 DD 4 HH 5 mm 6 ss 7 msec 8 Z 9 ± 10 tzHH 11 tzmm + if ((struct = /^(\d{4}|[+\-]\d{6})(?:-(\d{2})(?:-(\d{2}))?)?(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{3}))?)?(?:(Z)|([+\-])(\d{2})(?::(\d{2}))?)?)?$/.exec(date))) { + // avoid NaN timestamps caused by “undefined” values being passed to Date.UTC + for (var i = 0, k; (k = numericKeys[i]); ++i) { + struct[k] = +struct[k] || 0; + } + + // allow undefined days and months + struct[2] = (+struct[2] || 1) - 1; + struct[3] = +struct[3] || 1; + + if (struct[8] !== 'Z' && struct[9] !== undefined) { + minutesOffset = struct[10] * 60 + struct[11]; + + if (struct[9] === '+') { + minutesOffset = 0 - minutesOffset; + } + } + + timestamp = Date.UTC(struct[1], struct[2], struct[3], struct[4], struct[5] + minutesOffset, struct[6], struct[7]); + } + else { + timestamp = origParse ? origParse(date) : NaN; + } + + return timestamp; +}; + +if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Date) { + Date.parse = Ember.Date.parse; +} + +})(); + + + +(function() { + +})(); + + + +(function() { +var Evented = Ember.Evented, // ember-runtime/mixins/evented + Deferred = Ember.DeferredMixin, // ember-runtime/mixins/evented run = Ember.run, // ember-metal/run-loop get = Ember.get; // ember-metal/accessors -var LoadPromise = Ember.Mixin.create(Evented, DeferredMixin, { +var LoadPromise = Ember.Mixin.create(Evented, Deferred, { init: function() { this._super.apply(this, arguments); - this.one('didLoad', function() { - run(this, 'resolve', this); + + this.one('didLoad', this, function() { + this.resolve(this); + }); + + this.one('becameError', this, function() { + this.reject(this); }); if (get(this, 'isLoaded')) { @@ -37,6 +209,9 @@ DS.LoadPromise = LoadPromise; (function() { +/** +*/ + var get = Ember.get, set = Ember.set; var LoadPromise = DS.LoadPromise; // system/mixins/load_promise @@ -47,9 +222,19 @@ var LoadPromise = DS.LoadPromise; // system/mixins/load_promise time. You should not create record arrays yourself. Instead, an instance of DS.RecordArray or its subclasses will be returned by your application's store in response to queries. + + @module data + @submodule data-record-array + @main data-record-array + + @class RecordArray + @namespace DS + @extends Ember.ArrayProxy + @uses Ember.Evented + @uses DS.LoadPromise */ -DS.RecordArray = Ember.ArrayProxy.extend(Ember.Evented, LoadPromise, { +DS.RecordArray = Ember.ArrayProxy.extend(LoadPromise, { /** The model type contained by this record array. @@ -111,8 +296,19 @@ DS.RecordArray = Ember.ArrayProxy.extend(Ember.Evented, LoadPromise, { (function() { +/** + @module data + @submodule data-record-array +*/ + var get = Ember.get; +/** + @class FilteredRecordArray + @namespace DS + @extends DS.RecordArray + @constructor +*/ DS.FilteredRecordArray = DS.RecordArray.extend({ filterFunction: null, isLoaded: true, @@ -123,8 +319,8 @@ DS.FilteredRecordArray = DS.RecordArray.extend({ }, updateFilter: Ember.observer(function() { - var store = get(this, 'store'); - store.updateRecordArrayFilter(this, get(this, 'type'), get(this, 'filterFunction')); + var manager = get(this, 'manager'); + manager.updateFilter(this, get(this, 'type'), get(this, 'filterFunction')); }, 'filterFunction') }); @@ -133,8 +329,19 @@ DS.FilteredRecordArray = DS.RecordArray.extend({ (function() { +/** + @module data + @submodule data-record-array +*/ + var get = Ember.get, set = Ember.set; +/** + @class AdapterPopulatedRecordArray + @namespace DS + @extends DS.RecordArray + @constructor +*/ DS.AdapterPopulatedRecordArray = DS.RecordArray.extend({ query: null, @@ -144,18 +351,13 @@ DS.AdapterPopulatedRecordArray = DS.RecordArray.extend({ }, 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(); + this.setProperties({ + content: Ember.A(references), + isLoaded: true + }); - var self = this; // TODO: does triggering didLoad event should be the last action of the runLoop? - Ember.run.once(function() { - self.trigger('didLoad'); - }); + Ember.run.once(this, 'trigger', 'didLoad'); } }); @@ -164,6 +366,11 @@ DS.AdapterPopulatedRecordArray = DS.RecordArray.extend({ (function() { +/** + @module data + @submodule data-record-array +*/ + var get = Ember.get, set = Ember.set; /** @@ -195,6 +402,11 @@ var get = Ember.get, set = Ember.set; We call the record to which a relationship belongs the relationship's _owner_. + + @class ManyArray + @namespace DS + @extends DS.RecordArray + @constructor */ DS.ManyArray = DS.RecordArray.extend({ init: function() { @@ -211,6 +423,15 @@ DS.ManyArray = DS.RecordArray.extend({ */ owner: null, + /** + @private + + `true` if the relationship is polymorphic, `false` otherwise. + + @property {Boolean} + */ + isPolymorphic: false, + // LOADING STATE isLoaded: false, @@ -230,17 +451,16 @@ DS.ManyArray = DS.RecordArray.extend({ fetch: function() { var references = get(this, 'content'), store = get(this, 'store'), - type = get(this, 'type'), owner = get(this, 'owner'); - store.fetchUnloadedReferences(type, references, owner); + store.fetchUnloadedReferences(references, owner); }, // Overrides Ember.Array's replace method to implement replaceContent: function(index, removed, added) { // Map the array of record objects into an array of client ids. added = added.map(function(record) { - Ember.assert("You can only add records of " + (get(this, 'type') && get(this, 'type').toString()) + " to this relationship.", !get(this, 'type') || (get(this, 'type') === record.constructor)); + Ember.assert("You can only add records of " + (get(this, 'type') && get(this, 'type').toString()) + " to this relationship.", !get(this, 'type') || (get(this, 'type').detectInstance(record)) ); return get(record, '_reference'); }, this); @@ -328,6 +548,8 @@ DS.ManyArray = DS.RecordArray.extend({ type = get(this, 'type'), record; + Ember.assert("You can not create records of " + (get(this, 'type') && get(this, 'type').toString()) + " on this polymorphic relationship.", !get(this, 'isPolymorphic')); + transaction = transaction || get(owner, 'transaction'); record = store.createRecord.call(store, type, hash, transaction); @@ -343,14 +565,22 @@ DS.ManyArray = DS.RecordArray.extend({ (function() { +/** + @module data + @submodule data-record-array +*/ })(); (function() { -var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt, - removeObject = Ember.EnumerableUtils.removeObject, forEach = Ember.EnumerableUtils.forEach; +var get = Ember.get, set = Ember.set, forEach = Ember.EnumerableUtils.forEach; + +/** + @module data + @submodule data-transaction +*/ /** A transaction allows you to collect multiple records into a unit of work @@ -430,8 +660,6 @@ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt, calling commit. */ -var arrayDefault = function() { return []; }; - DS.Transaction = Ember.Object.extend({ /** @private @@ -440,15 +668,7 @@ DS.Transaction = Ember.Object.extend({ 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()); + set(this, 'records', Ember.OrderedSet.create()); }, /** @@ -475,11 +695,11 @@ DS.Transaction = Ember.Object.extend({ isDefault: Ember.computed(function() { return this === get(this, 'store.defaultTransaction'); - }), + }).volatile(), /** Adds an existing record to this transaction. Only records without - modficiations (i.e., records whose `isDirty` property is `false`) + modificiations (i.e., records whose `isDirty` property is `false`) can be added to a transaction. @param {DS.Model} record the record to add to the transaction @@ -487,29 +707,57 @@ DS.Transaction = Ember.Object.extend({ 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'); + var store = get(this, 'store'); + var adapter = get(store, '_adapter'); + var serializer = get(adapter, 'serializer'); + serializer.eachEmbeddedRecord(record, function(embeddedRecord, embeddedType) { + if (embeddedType === 'load') { return; } + + this.add(embeddedRecord); + }, this); - // Make `add` idempotent - if (recordTransaction === this) { return; } + this.adoptRecord(record); + }, - // XXX it should be possible to move a dirty transaction from the default transaction + relationships: Ember.computed(function() { + var relationships = Ember.OrderedSet.create(), + records = get(this, 'records'), + store = get(this, 'store'); - // 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')); + records.forEach(function(record) { + var reference = get(record, '_reference'); + var changes = store.relationshipChangesFor(reference); + for(var i = 0; i < changes.length; i++) { + relationships.add(changes[i]); + } + }); - Ember.assert("Models cannot belong to more than one transaction at a time.", recordTransaction === defaultTransaction); + return relationships; + }).volatile(), - this.adoptRecord(record); - }, + commitDetails: Ember.computed(function() { + var commitDetails = Ember.MapWithDefault.create({ + defaultValue: function() { + return { + created: Ember.OrderedSet.create(), + updated: Ember.OrderedSet.create(), + deleted: Ember.OrderedSet.create() + }; + } + }); - relationshipBecameDirty: function(relationship) { - get(this, 'relationships').add(relationship); - }, + var records = get(this, 'records'), + store = get(this, 'store'); - relationshipBecameClean: function(relationship) { - get(this, 'relationships').remove(relationship); - }, + records.forEach(function(record) { + if(!get(record, 'isDirty')) return; + record.send('willCommit'); + var adapter = store.adapterForType(record.constructor); + commitDetails.get(adapter)[get(record, 'dirtyType')].add(record); + }); + + return commitDetails; + }).volatile(), /** Commits the transaction, which causes all of the modified records that @@ -522,36 +770,22 @@ DS.Transaction = Ember.Object.extend({ */ 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) { + if (get(this, 'isDefault')) { 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); } - } + var commitDetails = get(this, 'commitDetails'), + relationships = get(this, 'relationships'); + + commitDetails.forEach(function(adapter, commitDetails) { + Ember.assert("You tried to commit records but you have no adapter", adapter); + Ember.assert("You tried to commit records but your adapter does not implement `commit`", adapter.commit); + + adapter.commit(store, commitDetails); + }); // Once we've committed the transaction, there is no need to // keep the OneToManyChanges around. Destroy them so they @@ -575,21 +809,36 @@ DS.Transaction = Ember.Object.extend({ 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); + // Destroy all relationship changes and compute + // all references affected + var references = Ember.OrderedSet.create(); + var relationships = get(this, 'relationships'); + relationships.forEach(function(r) { + references.add(r.firstRecordReference); + references.add(r.secondRecordReference); + r.destroy(); + }); + + var records = get(this, 'records'); + records.forEach(function(record) { + if (!record.get('isDirty')) return; + record.send('rollback'); + }); // Now that all records in the transaction are guaranteed to be // clean, migrate them all to the store's default transaction. this.removeCleanRecords(); + + // Remaining associated references are not part of the transaction, but + // can still have hasMany's which have not been reloaded + references.forEach(function(r) { + if (r && r.record) { + var record = r.record; + record.suspendRelationshipObservers(function() { + record.reloadHasManys(); + }); + } + }, this); }, /** @@ -615,27 +864,12 @@ DS.Transaction = Ember.Object.extend({ 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); + var records = get(this, 'records'); + records.forEach(function(record) { + if(!record.get('isDirty')) { + this.remove(record); + } + }, this); }, /** @@ -656,81 +890,51 @@ DS.Transaction = Ember.Object.extend({ var oldTransaction = get(record, 'transaction'); if (oldTransaction) { - oldTransaction.removeFromBucket('clean', record); + oldTransaction.removeRecord(record); } - this.addToBucket('clean', record); + get(this, 'records').add(record); set(record, 'transaction', this); }, /** - @private - - Adds a record to the named bucket. + @private - @param {String} bucketType one of `clean`, `created`, `updated`, or `deleted` + Removes the record without performing the normal checks + to ensure that the record is re-added to the store's + default transaction. */ - 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. + removeRecord: function(record) { + get(this, 'records').remove(record); + } - @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); - }, +DS.Transaction.reopenClass({ + ensureSameTransaction: function(records){ + var transactions = Ember.A(); + forEach( records, function(record){ + if (record){ transactions.pushObject(get(record, 'transaction')); } + }); - /** - @private + 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); + } - 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. + return prev; + }, null); - @param {String} bucketType one of `created`, `updated`, or `deleted` - */ - recordBecameClean: function(kind, record) { - this.removeFromBucket(kind, record); - this.remove(record); - } + if (transaction) { + forEach( records, function(record){ + if (record){ transaction.add(record); } + }); + } else { + transaction = transactions.objectAt(0); + } + return transaction; + } }); })(); @@ -738,11 +942,9 @@ DS.Transaction = Ember.Object.extend({ (function() { -var classify = Ember.String.classify, get = Ember.get; +var get = Ember.get; /** -@private - The Mappable mixin is designed for classes that would like to behave as a map for configuration purposes. @@ -791,11 +993,16 @@ var classify = Ember.String.classify, get = Ember.get; }) }); - The function passed to `generateMapFunctionFor` is invoked every time a - new value is added to the map. + The function passed to `generateMapFunctionFor` is invoked every time a + new value is added to the map. + + @class _Mappable + @private + @namespace DS + @extends Ember.Mixin **/ -var resolveMapConflict = function(oldValue, newValue, mappingsKey) { +var resolveMapConflict = function(oldValue, newValue) { return oldValue; }; @@ -809,7 +1016,7 @@ var transformMapValue = function(key, value) { DS._Mappable = Ember.Mixin.create({ createInstanceMapFor: function(mapName) { - var instanceMeta = Ember.metaPath(this, ['DS.Mappable'], true); + var instanceMeta = getMappableMeta(this); instanceMeta.values = instanceMeta.values || {}; @@ -829,7 +1036,7 @@ DS._Mappable = Ember.Mixin.create({ }, _copyMap: function(mapName, klass, instanceMap) { - var classMeta = Ember.metaPath(klass, ['DS.Mappable'], true); + var classMeta = getMappableMeta(klass); var classMap = classMeta[mapName]; if (classMap) { @@ -844,7 +1051,7 @@ DS._Mappable = Ember.Mixin.create({ var newValue = transformedValue; if (oldValue) { - newValue = (this.constructor.resolveMapConflict || resolveMapConflict)(oldValue, newValue, mapName); + newValue = (this.constructor.resolveMapConflict || resolveMapConflict)(oldValue, newValue); } instanceMap.set(transformedKey, newValue); @@ -856,7 +1063,8 @@ DS._Mappable = Ember.Mixin.create({ DS._Mappable.generateMapFunctionFor = function(mapName, transform) { return function(key, value) { - var meta = Ember.metaPath(this, ['DS.Mappable'], true); + var meta = getMappableMeta(this); + var map = meta[mapName] || Ember.MapWithDefault.create({ defaultValue: function() { return {}; } }); @@ -867,6 +1075,20 @@ DS._Mappable.generateMapFunctionFor = function(mapName, transform) { }; }; +function getMappableMeta(obj) { + var meta = Ember.meta(obj, true), + keyName = 'DS.Mappable', + value = meta[keyName]; + + if (!value) { meta[keyName] = {}; } + + if (!meta.hasOwnProperty(keyName)) { + meta[keyName] = Ember.create(meta[keyName]); + } + + return meta[keyName]; +} + })(); @@ -874,8 +1096,17 @@ DS._Mappable.generateMapFunctionFor = function(mapName, transform) { (function() { /*globals Ember*/ /*jshint eqnull:true*/ -var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt, once = Ember.run.once; +/** + @module data + @submodule data-store +*/ + +var get = Ember.get, set = Ember.set; +var once = Ember.run.once; +var isNone = Ember.isNone; var forEach = Ember.EnumerableUtils.forEach; +var map = Ember.EnumerableUtils.map; + // These values are used in the data cache when clientIds are // needed but the underlying data has not yet been loaded by // the server. @@ -890,10 +1121,13 @@ var CREATED = { created: true }; // scheme: // // * +id+ means an identifier managed by an external source, provided inside -// the data provided by that source. +// the data provided by that source. These are always coerced to be strings +// before being used internally. // * +clientId+ means a transient numerical identifier generated at runtime by // the data store. It is important primarily because newly created objects may // not yet have an externally generated id. +// * +reference+ means a record reference object, which holds metadata about a +// record, even if it has not yet been fully materialized. // * +type+ means a subclass of DS.Model. // Used by the store to normalize IDs entering the store. Despite the fact @@ -906,32 +1140,48 @@ 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 + It is also responsible for creating instances of DS.Model that wrap the individual data for a record, so that they can be bound to in your Handlebars templates. - Create a new store like this: + Define your application's store like this: + + MyApp.Store = DS.Store.extend(); - MyApp.store = DS.Store.create(); + Most Ember.js applications will only have a single `DS.Store` that is + automatically created by their `Ember.Application`. - You can retrieve DS.Model instances from the store in several ways. To retrieve - a record for a specific id, use the `find()` method: + You can retrieve models from the store in several ways. To retrieve a record + for a specific id, use `DS.Model`'s `find()` method: - var record = MyApp.store.find(MyApp.Contact, 123); + var person = App.Person.find(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: + If your application has multiple `DS.Store` instances (an unusual case), you can + specify which store should be used: + + var person = store.find(App.Person, 123); + + In general, you should retrieve models using the methods on `DS.Model`; you should + rarely need to interact with the store directly. + + By default, the store will talk to your backend using a standard REST mechanism. + You can customize how the store talks to your backend by specifying a custom adapter: MyApp.store = DS.Store.create({ adapter: 'MyApp.CustomAdapter' }); - You can learn more about writing a custom adapter by reading the `DS.Adapter` - documentation. + You can learn more about writing a custom adapter by reading the `DS.Adapter` + documentation. + + @class Store + @namespace DS + @extends Ember.Object + @uses DS._Mappable + @constructor */ DS.Store = Ember.Object.extend(DS._Mappable, { @@ -939,42 +1189,23 @@ 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. + 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.recordArrayManager = DS.RecordArrayManager.create({ + store: this + }); 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, 'currentTransaction', this.transaction()); set(this, 'defaultTransaction', this.transaction()); }, @@ -999,31 +1230,6 @@ DS.Store = Ember.Object.extend(DS._Mappable, { 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 @@ -1042,48 +1248,23 @@ DS.Store = Ember.Object.extend(DS._Mappable, { @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 reference = get(record, '_reference'), + data = reference.data, + adapter = this.adapterForType(record.constructor); - var prematerialized = this.clientIdToPrematerializedData[clientId]; + reference.data = MATERIALIZED; - // 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); + adapter.materialize(record, data, reference.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 @@ -1091,7 +1272,14 @@ DS.Store = Ember.Object.extend(DS._Mappable, { @property {DS.Adapter|String} */ - adapter: 'DS.RESTAdapter', + adapter: Ember.computed(function(){ + if (!Ember.testing) { + Ember.debug("A custom DS.Adapter was not provided as the 'Adapter' property of your application's Store. The default (DS.RESTAdapter) will be used."); + } + + return 'DS.RESTAdapter'; + }).property(), + /** @private @@ -1165,6 +1353,7 @@ DS.Store = Ember.Object.extend(DS._Mappable, { If you want to create a record inside of a given transaction, use `transaction.createRecord()` instead of `store.createRecord()`. + @method createRecord @param {subclass of DS.Model} type @param {Object} properties a hash of properties to set on the newly created record. @@ -1197,15 +1386,17 @@ DS.Store = Ember.Object.extend(DS._Mappable, { // 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 (isNone(id)) { + var adapter = this.adapterForType(type); + if (adapter && adapter.generateIdForRecord) { id = coerceId(adapter.generateIdForRecord(this, record)); properties.id = id; } } + // Coerce ID to a string id = coerceId(id); // Create a new `clientId` and associate it with the @@ -1214,24 +1405,20 @@ DS.Store = Ember.Object.extend(DS._Mappable, { // 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); + var reference = this.createReference(type, id); + reference.data = CREATED; - // Now that we have a clientId, attach it to the record we + // Now that we have a reference, attach it to the record we // just created. - set(record, 'clientId', clientId); + set(record, '_reference', reference); + reference.record = record; // 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); @@ -1320,6 +1507,10 @@ DS.Store = Ember.Object.extend(DS._Mappable, { You can check whether a query results `RecordArray` has loaded by checking its `isLoaded` property. + + @method find + @param {DS.Model} type + @param {Object|String|Integer|null} id */ find: function(type, id) { if (id === undefined) { @@ -1347,22 +1538,42 @@ DS.Store = Ember.Object.extend(DS._Mappable, { `getByReference`. */ findById: function(type, id) { - var clientId = this.typeMapFor(type).idToCid[id]; + var reference; - if (clientId) { - return this.findByClientId(type, clientId); + if (this.hasReferenceForId(type, id)) { + reference = this.referenceForId(type, id); + + if (reference.data !== UNLOADED) { + return this.recordForReference(reference); + } } - clientId = this.pushData(LOADING, id, type); + if (!reference) { + reference = this.createReference(type, id); + } + + reference.data = LOADING; // create a new instance of the model type in the // 'isLoading' state - var record = this.materializeRecord(type, clientId, id); + var record = this.materializeRecord(reference); - // 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"; } + if (reference.data === LOADING) { + // let the adapter set the data, possibly async + var adapter = this.adapterForType(type), + store = this; + + Ember.assert("You tried to find a record but you have no adapter (for " + type + ")", adapter); + Ember.assert("You tried to find a record but your adapter does not implement `find`", adapter.find); + + var thenable = adapter.find(this, type, id); + + if (thenable && thenable.then) { + thenable.then(null /* for future use */, function(error) { + store.recordWasError(record); + }); + } + } return record; }, @@ -1370,56 +1581,45 @@ DS.Store = Ember.Object.extend(DS._Mappable, { reloadRecord: function(record) { var type = record.constructor, adapter = this.adapterForType(type), + store = this, id = get(record, 'id'); Ember.assert("You cannot update a record without an ID", id); Ember.assert("You tried to update a record but you have no adapter (for " + type + ")", adapter); Ember.assert("You tried to update a record but your adapter does not implement `find`", adapter.find); - adapter.find(this, type, id); + var thenable = adapter.find(this, type, id); + + if (thenable && thenable.then) { + thenable.then(null /* for future use */, function(error) { + store.recordWasError(record); + }); + } }, /** @private - This method returns a record for a given 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: + This method returns a record for a given record refeence. - * 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 + If no record for the reference has yet been materialized, this method will + materialize a new `DS.Model` instance. This allows adapters to eagerly load + large amounts of data into the store, and avoid incurring the cost of + creating models until they are requested. In short, it's a convenient way to get a record for a known - clientId, materializing it if necessary. + record reference, materializing it if necessary. - @param {Class} type - @param {Number|String} clientId + @param {Object} reference + @returns {DS.Model} */ - findByClientId: function(type, clientId) { - var cidToData, record, id; - - record = this.recordCache[clientId]; + recordForReference: function(reference) { + var record = reference.record; 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(); - } + record = this.materializeRecord(reference); } return record; @@ -1428,27 +1628,25 @@ DS.Store = Ember.Object.extend(DS._Mappable, { /** @private - Given a type and array of `clientId`s, determines which of those + Given an array of `reference`s, determines which of those `clientId`s has not yet been loaded. In preparation for loading, this method also marks any unloaded `clientId`s as loading. */ - neededReferences: function(type, references) { - var neededReferences = [], - cidToData = this.clientIdToData, - reference; + unloadedReferences: function(references) { + var unloadedReferences = []; for (var i=0, l=references.length; i<l; i++) { - reference = references[i]; + var reference = references[i]; - if (cidToData[reference.clientId] === UNLOADED) { - neededReferences.push(reference); - cidToData[reference.clientId] = LOADING; + if (reference.data === UNLOADED) { + unloadedReferences.push(reference); + reference.data = LOADING; } } - return neededReferences; + return unloadedReferences; }, /** @@ -1457,61 +1655,72 @@ DS.Store = Ember.Object.extend(DS._Mappable, { 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. + First, it determines which of its `reference`s are still unloaded, + then invokes `findMany` on the adapter. */ - fetchUnloadedReferences: function(type, references, owner) { - var neededReferences = this.neededReferences(type, references); - this.fetchMany(type, neededReferences, owner); + fetchUnloadedReferences: function(references, owner) { + var unloadedReferences = this.unloadedReferences(references); + this.fetchMany(unloadedReferences, 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` + This method takes a list of `reference`s, groups the `reference`s by type, + converts the `reference`s into IDs, and then invokes the adapter's `findMany` method. + The `reference`s are grouped by type to invoke `findMany` on adapters + for each unique type in `reference`s. + It is used both by a brand new relationship (via the `findMany` method) or when the data underlying an existing relationship changes (via the `fetchUnloadedReferences` method). */ - fetchMany: function(type, references, owner) { + fetchMany: function(references, owner) { if (!references.length) { return; } - var ids = map(references, function(reference) { - return reference.id; + // Group By Type + var referencesByTypeMap = Ember.MapWithDefault.create({ + defaultValue: function() { return Ember.A(); } + }); + forEach(references, function(reference) { + referencesByTypeMap.get(reference.type).push(reference); }); - 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"; } - }, + forEach(referencesByTypeMap, function(type) { + var references = referencesByTypeMap.get(type), + ids = map(references, function(reference) { return reference.id; }); - referenceForId: function(type, id) { - var clientId = this.clientIdForId(type, id); - return this.referenceForClientId(clientId); - }, + var adapter = this.adapterForType(type); - referenceForClientId: function(clientId) { - var references = this.recordReferences; + Ember.assert("You tried to load many records but you have no adapter (for " + type + ")", adapter); + Ember.assert("You tried to load many records but your adapter does not implement `findMany`", adapter.findMany); - if (references[clientId]) { - return references[clientId]; - } + adapter.findMany(this, type, ids, owner); + }, this); + }, - var type = this.clientIdToType[clientId]; + hasReferenceForId: function(type, id) { + id = coerceId(id); - return references[clientId] = { - id: this.idForClientId(clientId), - clientId: clientId, - type: type - }; + return !!this.typeMapFor(type).idToReference[id]; }, - recordForReference: function(reference) { - return this.findByClientId(reference.type, reference.clientId); + referenceForId: function(type, id) { + id = coerceId(id); + + // Check to see if we have seen this type/id pair before. + var reference = this.typeMapFor(type).idToReference[id]; + + // If not, create a reference for it but don't populate it + // with any data yet. + if (!reference) { + reference = this.createReference(type, id); + reference.data = UNLOADED; + } + + return reference; }, /** @@ -1528,32 +1737,36 @@ DS.Store = Ember.Object.extend(DS._Mappable, { * 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` + * insert the unloaded references into the `loadingRecordArrays` bookkeeping structure, which will allow the `ManyArray` to know when all of its loading elements are loaded from the server. * ask the adapter to load the unloaded elements, by invoking findMany with the still-unloaded IDs. */ - findMany: function(type, 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 + findMany: function(type, idsOrReferencesOrOpaque, record, relationship) { + // 1. Determine which of the client ids need to be loaded + // 2. Create a new ManyArray whose content is ALL of the clientIds + // 3. Decrement the ManyArray's counter by the number of loaded clientIds + // 4. Put the ManyArray into our bookkeeping data structure, keyed on // the needed clientIds - // 6. Ask the adapter to load the records for the unloaded clientIds (but + // 5. Ask the adapter to load the records for the unloaded clientIds (but // convert them back to ids) - if (!Ember.isArray(ids)) { + if (!Ember.isArray(idsOrReferencesOrOpaque)) { 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()); + if (adapter && adapter.findHasMany) { + adapter.findHasMany(this, record, relationship, idsOrReferencesOrOpaque); + } else if (idsOrReferencesOrOpaque !== undefined) { + Ember.assert("You tried to load many records but you have no adapter (for " + type + ")", adapter); + Ember.assert("You tried to load many records but your adapter does not implement `findHasMany`", adapter.findHasMany); + } + + return this.recordArrayManager.createManyArray(type, Ember.A()); } // Coerce server IDs into Record Reference - var references = map(ids, function(reference) { + var references = map(idsOrReferencesOrOpaque, function(reference) { if (typeof reference !== 'object' && reference !== null) { return this.referenceForId(type, reference); } @@ -1561,32 +1774,26 @@ DS.Store = Ember.Object.extend(DS._Mappable, { return reference; }, this); - var neededReferences = this.neededReferences(type, references), - manyArray = this.createManyArray(type, Ember.A(references)), - loadingRecordArrays = this.loadingRecordArrays, - reference, clientId, i, l; + var unloadedReferences = this.unloadedReferences(references), + manyArray = this.recordArrayManager.createManyArray(type, Ember.A(references)), + reference, i, l; // Start the decrementing counter on the ManyArray at the number of // records we need to load from the adapter - manyArray.loadingRecordsCount(neededReferences.length); + manyArray.loadingRecordsCount(unloadedReferences.length); - if (neededReferences.length) { - for (i=0, l=neededReferences.length; i<l; i++) { - reference = neededReferences[i]; - clientId = reference.clientId; + if (unloadedReferences.length) { + for (i=0, l=unloadedReferences.length; i<l; i++) { + reference = unloadedReferences[i]; // keep track of the record arrays that a given loading record // is part of. This way, if the same record is in multiple // ManyArrays, all of their loading records counters will be // decremented when the adapter provides the data. - if (loadingRecordArrays[clientId]) { - loadingRecordArrays[clientId].push(manyArray); - } else { - this.loadingRecordArrays[clientId] = [ manyArray ]; - } + this.recordArrayManager.registerWaitingRecordArray(manyArray, reference); } - this.fetchMany(type, neededReferences, record); + this.fetchMany(unloadedReferences, record); } else { // all requested records are available manyArray.set('isLoaded', true); @@ -1600,8 +1807,6 @@ DS.Store = Ember.Object.extend(DS._Mappable, { }, /** - @private - This method delegates a query to the adapter. This is the one place where adapter-level semantics are exposed to the application. @@ -1609,6 +1814,8 @@ DS.Store = Ember.Object.extend(DS._Mappable, { language for all server-side queries, and then require all adapters to implement them. + @private + @method findQuery @param {Class} type @param {Object} query an opaque query to be used by the adapter @return {DS.AdapterPopulatedRecordArray} @@ -1616,8 +1823,12 @@ DS.Store = Ember.Object.extend(DS._Mappable, { 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"; } + + Ember.assert("You tried to load a query but you have no adapter (for " + type + ")", adapter); + Ember.assert("You tried to load a query but your adapter does not implement `findQuery`", adapter.findQuery); + + adapter.findQuery(this, type, query, array); + return array; }, @@ -1632,28 +1843,31 @@ DS.Store = Ember.Object.extend(DS._Mappable, { @return {DS.AdapterPopulatedRecordArray} */ findAll: function(type) { - var array = this.all(type); - this.fetchAll(type, array); - return array; + return this.fetchAll(type, this.all(type)); }, /** @private */ fetchAll: function(type, array) { - var sinceToken = this.typeMapFor(type).sinceToken, - adapter = this.adapterForType(type); + var adapter = this.adapterForType(type), + sinceToken = this.typeMapFor(type).metadata.since; 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"; } + Ember.assert("You tried to load all records but you have no adapter (for " + type + ")", adapter); + Ember.assert("You tried to load all records but your adapter does not implement `findAll`", adapter.findAll); + + adapter.findAll(this, type, sinceToken); + + return array; }, /** */ - sinceForType: function(type, sinceToken) { - this.typeMapFor(type).sinceToken = sinceToken; + metaForType: function(type, property, data) { + var target = this.typeMapFor(type).metadata; + set(target, property, data); }, /** @@ -1673,6 +1887,7 @@ DS.Store = Ember.Object.extend(DS._Mappable, { Also note that multiple calls to `all` for a given type will always return the same RecordArray. + @method all @param {Class} type @return {DS.RecordArray} */ @@ -1682,8 +1897,14 @@ DS.Store = Ember.Object.extend(DS._Mappable, { if (findAllCache) { return findAllCache; } - var array = DS.RecordArray.create({ type: type, content: Ember.A([]), store: this, isLoaded: true }); - this.registerRecordArray(array, type); + var array = DS.RecordArray.create({ + type: type, + content: Ember.A([]), + store: this, + isLoaded: true + }); + + this.recordArrayManager.registerFilteredRecordArray(array, type); typeMap.findAllCache = array; return array; @@ -1713,9 +1934,9 @@ DS.Store = Ember.Object.extend(DS._Mappable, { In this scenario, you might want to consider filtering the raw data before loading it into the store. + @method filter @param {Class} type @param {Function} filter - @return {DS.FilteredRecordArray} */ filter: function(type, query, filter) { @@ -1726,9 +1947,15 @@ DS.Store = Ember.Object.extend(DS._Mappable, { filter = query; } - var array = DS.FilteredRecordArray.create({ type: type, content: Ember.A([]), store: this, filterFunction: filter }); + var array = DS.FilteredRecordArray.create({ + type: type, + content: Ember.A([]), + store: this, + manager: this.recordArrayManager, + filterFunction: filter + }); - this.registerRecordArray(array, type, filter); + this.recordArrayManager.registerFilteredRecordArray(array, type, filter); return array; }, @@ -1743,7 +1970,8 @@ DS.Store = Ember.Object.extend(DS._Mappable, { @return {boolean} */ recordIsLoaded: function(type, id) { - return !Ember.isNone(this.typeMapFor(type).idToCid[id]); + if (!this.hasReferenceForId(type, id)) { return false; } + return typeof this.referenceForId(type, id).data === 'object'; }, // ............ @@ -1776,64 +2004,42 @@ DS.Store = Ember.Object.extend(DS._Mappable, { if (get(record, 'isDeleted')) { return; } - var cidToData = this.clientIdToData, - clientId = reference.clientId, - data = cidToData[clientId]; - - if (typeof data === "object") { - this.updateRecordArrays(type, clientId); + if (typeof reference.data === "object") { + this.recordArrayManager.referenceDidChange(reference); } }, - // ................. - // . 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 + This method delegates saving to the store's implicit transaction. Calling this method is essentially a request to persist any changes to records that were not explicitly added to a transaction. */ - commit: function() { + save: function() { + once(this, 'commitDefaultTransaction'); + }, + commit: Ember.aliasMethod('save'), + + commitDefaultTransaction: function() { get(this, 'defaultTransaction').commit(); }, + scheduleSave: function(record) { + get(this, 'currentTransaction').add(record); + once(this, 'flushSavedRecords'); + }, + + flushSavedRecords: function() { + get(this, 'currentTransaction').commit(); + set(this, 'currentTransaction', this.transaction()); + }, + /** Adapters should call this method if they would like to acknowledge that all changes related to a record (other than relationship @@ -1869,14 +2075,14 @@ DS.Store = Ember.Object.extend(DS._Mappable, { @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); } + + record.adapterDidCommit(); }, /** @@ -2046,7 +2252,9 @@ DS.Store = Ember.Object.extend(DS._Mappable, { @param {DS.Model} relationshipName */ didUpdateRelationship: function(record, relationshipName) { - var relationship = this.relationshipChangeFor(get(record, 'clientId'), relationshipName); + var clientId = get(record, '_reference').clientId; + + var relationship = this.relationshipChangeFor(clientId, relationshipName); //TODO(Igor) if (relationship) { relationship.adapterDidUpdate(); } }, @@ -2111,11 +2319,7 @@ DS.Store = Ember.Object.extend(DS._Mappable, { @param {Object} data */ updateRecordData: function(record, data) { - var clientId = get(record, 'clientId'), - cidToData = this.clientIdToData; - - cidToData[clientId] = data; - + get(record, '_reference').data = data; record.didChangeData(); }, @@ -2130,17 +2334,16 @@ DS.Store = Ember.Object.extend(DS._Mappable, { @param {Object} data */ updateId: function(record, data) { - var typeMap = this.typeMapFor(record.constructor), - clientId = get(record, 'clientId'), + var type = record.constructor, + typeMap = this.typeMapFor(type), + reference = get(record, '_reference'), 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; + typeMap.idToReference[id] = reference; + reference.id = id; }, /** @@ -2160,266 +2363,27 @@ DS.Store = Ember.Object.extend(DS._Mappable, { 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 + Returns a map of IDs to client IDs for a given type. */ - 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 + typeMapFor: function(type) { + var typeMaps = get(this, 'typeMaps'), + guid = Ember.guidFor(type), + typeMap; - 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; + typeMap = typeMaps[guid]; - return map(ids, function(id) { - id = coerceId(id); + if (typeMap) { return typeMap; } - var clientId = idToClientIdMap[id]; - if (clientId) { return clientId; } - return this.pushData(UNLOADED, id, type); - }, this); - }, + typeMap = { + idToReference: {}, + references: [], + metadata: {} + }; - typeForClientId: function(clientId) { - return this.clientIdToType[clientId]; - }, + typeMaps[guid] = typeMap; - idForClientId: function(clientId) { - return this.clientIdToId[clientId]; + return typeMap; }, // ................ @@ -2455,31 +2419,18 @@ DS.Store = Ember.Object.extend(DS._Mappable, { 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 reference = this.referenceForId(type, id); - var record = this.recordCache[clientId]; - if (record) { - once(record, 'loadedData'); - } - } else { - clientId = this.pushData(data, id, type); - cidToPrematerialized[clientId] = prematerialized; + if (reference.record) { + once(reference.record, 'loadedData'); } - this.updateRecordArraysLater(type, clientId); + reference.data = data; + reference.prematerialized = prematerialized; - return this.referenceForClientId(clientId); - }, + this.recordArrayManager.referenceDidChange(reference); - prematerialize: function(reference, prematerialized) { - this.clientIdToPrematerializedData[reference.clientId] = prematerialized; + return reference; }, loadMany: function(type, ids, dataList) { @@ -2496,7 +2447,12 @@ DS.Store = Ember.Object.extend(DS._Mappable, { }, loadHasMany: function(record, key, ids) { - record.materializeHasMany(key, ids); + //It looks sad to have to do the conversion in the store + var type = record.get(key + '.type'), + tuples = map(ids, function(id) { + return {id: id, type: type}; + }); + record.materializeHasMany(key, tuples); // Update any existing many arrays that use the previous IDs, // if necessary. @@ -2514,77 +2470,73 @@ DS.Store = Ember.Object.extend(DS._Mappable, { /** @private - Stores data for the specified type and id combination and returns - the client id. + Creates a new reference for a given type & ID pair. Metadata about the + record can be stored in the reference without having to create a full-blown + DS.Model instance. - @param {Object} data - @param {String|Number} id @param {DS.Model} type - @returns {Number} + @param {String|Number} id + @returns {Reference} */ - 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]); + createReference: function(type, id) { + var typeMap = this.typeMapFor(type), + idToReference = typeMap.idToReference; - var clientId = ++this.clientIdCounter; + Ember.assert('The id ' + id + ' has already been used with another record of type ' + type.toString() + '.', !id || !idToReference[id]); - cidToData[clientId] = data; - clientIdToTypeMap[clientId] = type; + var reference = { + id: id, + clientId: this.clientIdCounter++, + type: type + }; // if we're creating an item, this process will be done // later, once the object has been persisted. if (id) { - idToClientIdMap[id] = clientId; - clientIdToIdMap[clientId] = id; + idToReference[id] = reference; } - clientIds.push(clientId); + typeMap.references.push(reference); - return clientId; + return reference; }, // .......................... // . RECORD MATERIALIZATION . // .......................... - materializeRecord: function(type, clientId, id) { - var record; - - this.recordCache[clientId] = record = type._create({ + materializeRecord: function(reference) { + var record = reference.type._create({ + id: reference.id, store: this, - clientId: clientId + _reference: reference }); - set(record, 'id', id); + reference.record = record; get(this, 'defaultTransaction').adoptRecord(record); record.loadingData(); + + if (typeof reference.data === 'object') { + record.loadedData(); + } + return record; }, dematerializeRecord: function(record) { - var id = get(record, 'id'), - clientId = get(record, 'clientId'), - type = this.typeForClientId(clientId), + var reference = get(record, '_reference'), + type = reference.type, + id = reference.id, 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.idToReference[id]; } - if (id) { delete typeMap.idToCid[id]; } + var loc = typeMap.references.indexOf(reference); + typeMap.references.splice(loc, 1); }, willDestroy: function() { @@ -2625,7 +2577,9 @@ DS.Store = Ember.Object.extend(DS._Mappable, { delete changes[clientId][parentClientId][key][type]; }, - relationshipChangeFor: function(clientId, childKey, parentClientId, parentKey, type) { + relationshipChangeFor: function(clientReference, childKey, parentReference, parentKey, type) { + var clientId = clientReference.clientId, + parentClientId = parentReference ? parentReference.clientId : parentReference; var changes = this.relationshipChanges; var key = childKey + parentKey; if (!(clientId in changes) || !(parentClientId in changes[clientId])){ @@ -2695,7 +2649,7 @@ DS.Store = Ember.Object.extend(DS._Mappable, { // .............................. recordAttributeDidChange: function(reference, attributeName, newValue, oldValue) { - var record = this.recordForReference(reference), + var record = reference.record, dirtySet = new Ember.OrderedSet(), adapter = this.adapterForType(record.constructor); @@ -2761,6 +2715,11 @@ DS.Store.reopenClass({ (function() { +/** + @module data + @submodule data-model +*/ + var get = Ember.get, set = Ember.set, once = Ember.run.once, arrayMap = Ember.ArrayPolyfills.map; @@ -2910,9 +2869,13 @@ var get = Ember.get, set = Ember.set, } }) - 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. + Note that enter and exit events are called once per transition. If the + current state changes, but changes to another child state of the parent, + the transition event on the parent will not be triggered. + + @class States + @namespace DS + @extends Ember.State */ var stateProperty = Ember.computed(function(key) { @@ -2939,16 +2902,17 @@ 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; + get(manager, 'record')._changesToSync[context.name] = change; }; var didSetProperty = function(manager, context) { - var change = get(manager, 'record')._changesToSync[context.attributeName]; + var change = get(manager, 'record')._changesToSync[context.name]; change.value = get(get(manager, 'record'), context.name); change.sync(); }; DS.State = Ember.State.extend({ + isLoading: stateProperty, isLoaded: stateProperty, isReloading: stateProperty, isDirty: stateProperty, @@ -3020,15 +2984,6 @@ var DirtyState = DS.State.extend({ // 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, @@ -3041,24 +2996,15 @@ var DirtyState = DS.State.extend({ }, becameClean: function(manager) { - var record = get(manager, 'record'), - dirtyType = get(this, 'dirtyType'); + var record = get(manager, 'record'); record.withTransaction(function(t) { - t.recordBecameClean(dirtyType, record); + t.remove(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'); }, @@ -3076,29 +3022,32 @@ var DirtyState = DS.State.extend({ // TRANSITIONS enter: function(manager) { - var dirtyType = get(this, 'dirtyType'), - record = get(manager, 'record'); + var record = get(manager, 'record'); record.becameInFlight(); - - record.withTransaction(function (t) { - t.recordBecameInFlight(dirtyType, record); - }); }, // EVENTS + + materializingData: function(manager) { + set(manager, 'lastDirtyType', get(this, 'dirtyType')); + manager.transitionTo('materializing'); + }, + didCommit: function(manager) { var dirtyType = get(this, 'dirtyType'), record = get(manager, 'record'); record.withTransaction(function(t) { - t.recordBecameClean('inflight', record); + t.remove(record); }); manager.transitionTo('saved'); manager.send('invokeLifecycleCallbacks', dirtyType); }, + didChangeData: didChangeData, + becameInvalid: function(manager, errors) { var record = get(manager, 'record'); @@ -3122,12 +3071,12 @@ var DirtyState = DS.State.extend({ isValid: false, exit: function(manager) { - var record = get(manager, 'record'); + var record = get(manager, 'record'); - record.withTransaction(function (t) { - t.recordBecameClean('inflight', record); - }); - }, + record.withTransaction(function (t) { + t.remove(record); + }); + }, // EVENTS deleteRecord: function(manager) { @@ -3188,10 +3137,6 @@ 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'); } @@ -3208,18 +3153,15 @@ 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(); + record.clearRelationships(); } }); var states = { rootState: Ember.State.create({ // FLAGS + isLoading: false, isLoaded: false, isReloading: false, isDirty: false, @@ -3254,11 +3196,19 @@ var states = { // Usually, this process is asynchronous, using an // XHR to retrieve the data. loading: DS.State.create({ + // FLAGS + isLoading: true, + // EVENTS loadedData: didChangeData, materializingData: function(manager) { manager.transitionTo('loaded.materializing.firstTime'); + }, + + becameError: function(manager) { + manager.transitionTo('error'); + manager.send('invokeLifecycleCallbacks'); } }), @@ -3274,9 +3224,6 @@ var states = { // SUBSTATES materializing: DS.State.create({ - // FLAGS - isLoaded: false, - // EVENTS willSetProperty: Ember.K, didSetProperty: Ember.K, @@ -3289,6 +3236,9 @@ var states = { // SUBSTATES firstTime: DS.State.create({ + // FLAGS + isLoaded: false, + exit: function(manager) { var record = get(manager, 'record'); @@ -3353,8 +3303,22 @@ var states = { }, unloadRecord: function(manager) { + var record = get(manager, 'record'); + + // clear relationships before moving to deleted state + // otherwise it fails + record.clearRelationships(); manager.transitionTo('deleted.saved'); - get(manager, 'record').clearRelationships(); + }, + + didCommit: function(manager) { + var record = get(manager, 'record'); + + record.withTransaction(function(t) { + t.remove(record); + }); + + manager.send('invokeLifecycleCallbacks', get(manager, 'lastDirtyType')); }, invokeLifecycleCallbacks: function(manager, dirtyType) { @@ -3364,6 +3328,8 @@ var states = { } else { record.trigger('didUpdate', record); } + + record.trigger('didCommit', record); } }), @@ -3393,7 +3359,7 @@ var states = { var record = get(manager, 'record'), store = get(record, 'store'); - store.removeFromRecordArrays(record); + store.recordArrayManager.remove(record); }, // SUBSTATES @@ -3402,14 +3368,6 @@ var states = { // 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) { @@ -3426,9 +3384,8 @@ var states = { var record = get(manager, 'record'); record.withTransaction(function(t) { - t.recordBecameClean('deleted', record); + t.remove(record); }); - manager.transitionTo('loaded.materializing'); } }), @@ -3446,10 +3403,6 @@ var states = { var record = get(manager, 'record'); record.becameInFlight(); - - record.withTransaction(function (t) { - t.recordBecameInFlight('deleted', record); - }); }, // EVENTS @@ -3457,7 +3410,7 @@ var states = { var record = get(manager, 'record'); record.withTransaction(function(t) { - t.recordBecameClean('inflight', record); + t.remove(record); }); manager.transitionTo('saved'); @@ -3483,6 +3436,7 @@ var states = { invokeLifecycleCallbacks: function(manager) { var record = get(manager, 'record'); record.trigger('didDelete', record); + record.trigger('didCommit', record); } }) }), @@ -3534,7 +3488,22 @@ var retrieveFromCurrentState = Ember.computed(function(key, value) { return get(get(this, 'stateManager.currentState'), key); }).property('stateManager.currentState').readOnly(); +/** + + The model class that all Ember Data records descend from. + + @module data + @submodule data-model + @main data-model + + @class Model + @namespace DS + @extends Ember.Object + @constructor +*/ + DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, { + isLoading: retrieveFromCurrentState, isLoaded: retrieveFromCurrentState, isReloading: retrieveFromCurrentState, isDirty: retrieveFromCurrentState, @@ -3543,6 +3512,7 @@ DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, { isError: retrieveFromCurrentState, isNew: retrieveFromCurrentState, isValid: retrieveFromCurrentState, + dirtyType: retrieveFromCurrentState, clientId: null, id: null, @@ -3554,12 +3524,12 @@ DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, { Create a JSON representation of the record, using the serialization strategy of the store's adapter. - Available options: + @method serialize + @param {Object} options 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) { @@ -3567,22 +3537,75 @@ DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, { return store.serialize(this, options); }, + /** + Use {{#crossLink "DS.JSONSerializer"}}DS.JSONSerializer{{/crossLink}} to + get the JSON representation of a record. + + @method toJSON + @param {Object} options Available options: + + * `includeId`: `true` if the record's ID should be included in the + JSON representation. + + @returns {Object} A JSON representation of the object. + */ toJSON: function(options) { var serializer = DS.JSONSerializer.create(); return serializer.serialize(this, options); }, + /** + Fired when the record is loaded from the server. + + @event didLoad + */ didLoad: Ember.K, + + /** + Fired when the record is reloaded from the server. + + @event didReload + */ didReload: Ember.K, + + /** + Fired when the record is updated. + + @event didUpdate + */ didUpdate: Ember.K, + + /** + Fired when the record is created. + + @event didCreate + */ didCreate: Ember.K, + + /** + Fired when the record is deleted. + + @event didDelete + */ didDelete: Ember.K, + + /** + Fired when the record becomes invalid. + + @event becameInvalid + */ becameInvalid: Ember.K, + + /** + Fired when the record enters the error state. + + @event becameError + */ becameError: Ember.K, data: Ember.computed(function() { if (!this._data) { - this.materializeData(); + this.setupData(); } return this._data; @@ -3612,7 +3635,6 @@ DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, { }, _setup: function() { - this._relationshipChanges = {}; this._changesToSync = {}; }, @@ -3637,21 +3659,6 @@ DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, { 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'); }, @@ -3664,13 +3671,10 @@ DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, { clearRelationships: function() { this.eachRelationship(function(name, relationship) { - // if the relationship is unmaterialized, move on - if (this.cacheFor(name) === undefined) { return; } - if (relationship.kind === 'belongsTo') { set(this, name, null); } else if (relationship.kind === 'hasMany') { - get(this, name).clear(); + this.clearHasMany(relationship); } }, this); }, @@ -3704,18 +3708,19 @@ DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, { }, dataDidChange: Ember.observer(function() { - var relationships = get(this.constructor, 'relationshipsByName'); + this.reloadHasManys(); + this.send('finishedMaterializing'); + }, 'data'), + reloadHasManys: function() { + var relationships = get(this.constructor, 'relationshipsByName'); this.updateRecordArraysLater(); - relationships.forEach(function(name, relationship) { if (relationship.kind === 'hasMany') { this.hasManyDidChange(relationship.key); } }, this); - - this.send('finishedMaterializing'); - }, 'data'), + }, hasManyDidChange: function(key) { var cachedValue = this.cacheFor(key); @@ -3726,8 +3731,15 @@ DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, { 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; } + if (typeof id === 'object') { + if( id.clientId ) { + // if it was already a reference, return the reference + return id; + } else { + // <id, type> tuple for a polymorphic association. + return store.referenceForId(id.type, id.id); + } + } return store.referenceForId(type, id); }); @@ -3739,7 +3751,7 @@ DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, { Ember.run.once(this, this.updateRecordArrays); }, - setupData: function(prematerialized) { + setupData: function() { this._data = { attributes: {}, belongsTo: {}, @@ -3761,12 +3773,45 @@ DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, { this._data.attributes[name] = value; }, - materializeHasMany: function(name, ids) { - this._data.hasMany[name] = ids; + materializeHasMany: function(name, tuplesOrReferencesOrOpaque) { + var tuplesOrReferencesOrOpaqueType = typeof tuplesOrReferencesOrOpaque; + + if (tuplesOrReferencesOrOpaque && tuplesOrReferencesOrOpaqueType !== 'string' && tuplesOrReferencesOrOpaque.length > 1) { + Ember.assert('materializeHasMany expects tuples, references or opaque token, not ' + tuplesOrReferencesOrOpaque[0], tuplesOrReferencesOrOpaque[0].hasOwnProperty('id') && tuplesOrReferencesOrOpaque[0].type); + } + + if( tuplesOrReferencesOrOpaqueType === "string" ) { + this._data.hasMany[name] = tuplesOrReferencesOrOpaque; + } else { + var references = tuplesOrReferencesOrOpaque; + + if (tuplesOrReferencesOrOpaque && Ember.isArray(tuplesOrReferencesOrOpaque)) { + references = this._convertTuplesToReferences(tuplesOrReferencesOrOpaque); + } + + this._data.hasMany[name] = references; + } + }, + + materializeBelongsTo: function(name, tupleOrReference) { + if (tupleOrReference) { Ember.assert('materializeBelongsTo expects a tuple or a reference, not a ' + tupleOrReference, !tupleOrReference || (tupleOrReference.hasOwnProperty('id') && tupleOrReference.hasOwnProperty('type'))); } + + this._data.belongsTo[name] = tupleOrReference; + }, + + _convertTuplesToReferences: function(tuplesOrReferences) { + return map(tuplesOrReferences, function(tupleOrReference) { + return this._convertTupleToReference(tupleOrReference); + }, this); }, - materializeBelongsTo: function(name, id) { - this._data.belongsTo[name] = id; + _convertTupleToReference: function(tupleOrReference) { + var store = get(this, 'store'); + if(tupleOrReference.clientId) { + return tupleOrReference; + } else { + return store.referenceForId(tupleOrReference.type, tupleOrReference.id); + } }, rollback: function() { @@ -3815,10 +3860,54 @@ DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, { becameInFlight: function() { }, - // FOR USE BY THE BASIC ADAPTER + /** + @private + + */ + resolveOn: function(successEvent) { + var model = this; + + return new Ember.RSVP.Promise(function(resolve, reject) { + function success() { + this.off('becameError', error); + this.off('becameInvalid', error); + resolve(this); + } + function error() { + this.off(successEvent, success); + reject(this); + } + + model.one(successEvent, success); + model.one('becameError', error); + model.one('becameInvalid', error); + }); + }, + + /** + Save the record. + @method save + */ save: function() { this.get('store').scheduleSave(this); + + return this.resolveOn('didCommit'); + }, + + /** + Reload the record from the adapter. + + This will only work if the record has already finished loading + and has not yet been modified (`isLoaded` but not `isDirty`, + or `isSaving`). + + @method reload + */ + reload: function() { + this.send('reloadRecord'); + + return this.resolveOn('didReload'); }, // FOR USE DURING COMMIT PROCESS @@ -3840,10 +3929,6 @@ DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, { this.updateRecordArraysLater(); }, - _reference: Ember.computed(function() { - return get(this, 'store').referenceForClientId(get(this, 'clientId')); - }), - adapterDidInvalidate: function(errors) { this.send('becameInvalid', errors); }, @@ -3880,18 +3965,67 @@ var storeAlias = function(methodName) { }; DS.Model.reopenClass({ - isLoaded: storeAlias('recordIsLoaded'), - find: storeAlias('find'), - all: storeAlias('all'), - query: storeAlias('findQuery'), - filter: storeAlias('filter'), + /** @private + Alias DS.Model's `create` method to `_create`. This allows us to create DS.Model + instances from within the store, but if end users accidentally call `create()` + (instead of `createRecord()`), we can raise an error. + */ _create: DS.Model.create, + /** @private + + Override the class' `create()` method to raise an error. This prevents end users + from inadvertently calling `create()` instead of `createRecord()`. The store is + still able to create instances by calling the `_create()` method. + */ create: function() { throw new Ember.Error("You should not call `create` on a model. Instead, call `createRecord` with the attributes you would like to set."); }, + /** + See {{#crossLink "DS.Store/find:method"}}`DS.Store.find()`{{/crossLink}}. + + @method find + @param {Object|String|Array|null} query A query to find records by. + + */ + find: storeAlias('find'), + + /** + See {{#crossLink "DS.Store/all:method"}}`DS.Store.all()`{{/crossLink}}. + + @method all + @return {DS.RecordArray} + */ + all: storeAlias('all'), + + /** + See {{#crossLink "DS.Store/findQuery:method"}}`DS.Store.findQuery()`{{/crossLink}}. + + @method query + @param {Object} query an opaque query to be used by the adapter + @return {DS.AdapterPopulatedRecordArray} + */ + query: storeAlias('findQuery'), + + /** + See {{#crossLink "DS.Store/filter:method"}}`DS.Store.filter()`{{/crossLink}}. + + @method filter + @param {Function} filter + @return {DS.FilteredRecordArray} + */ + filter: storeAlias('filter'), + + /** + See {{#crossLink "DS.Store/createRecord:method"}}`DS.Store.createRecord()`{{/crossLink}}. + + @method createRecord + @param {Object} properties a hash of properties to set on the + newly created record. + @returns DS.Model + */ createRecord: storeAlias('createRecord') }); @@ -3900,7 +4034,13 @@ DS.Model.reopenClass({ (function() { +/** + @module data + @submodule data-model +*/ + var get = Ember.get; + DS.Model.reopenClass({ attributes: Ember.computed(function() { var map = Ember.Map.create(); @@ -3918,29 +4058,6 @@ DS.Model.reopenClass({ }) }); -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) { @@ -3966,7 +4083,11 @@ function getAttr(record, options, key) { var value = attributes[key]; if (value === undefined) { - value = options.defaultValue; + if (typeof options.defaultValue === "function") { + value = options.defaultValue(); + } else { + value = options.defaultValue; + } } return value; @@ -4001,6 +4122,551 @@ DS.attr = function(type, options) { (function() { +/** + @module data + @submodule data-model +*/ + +})(); + + + +(function() { +/** + An AttributeChange object is created whenever a record's + attribute changes value. It is used to track changes to a + record between transaction commits. +*/ + +var AttributeChange = DS.AttributeChange = function(options) { + this.reference = options.reference; + this.store = options.store; + this.name = options.name; + this.oldValue = options.oldValue; +}; + +AttributeChange.createChange = function(options) { + return new AttributeChange(options); +}; + +AttributeChange.prototype = { + sync: function() { + this.store.recordAttributeDidChange(this.reference, this.name, this.value, this.oldValue); + + // TODO: Use this object in the commit process + this.destroy(); + }, + + /** + If the AttributeChange is destroyed (either by being rolled back + or being committed), remove it from the list of pending changes + on the record. + */ + destroy: function() { + var record = this.reference.record; + + delete record._changesToSync[this.name]; + } +}; + +})(); + + + +(function() { +var get = Ember.get, set = Ember.set; +var forEach = Ember.EnumerableUtils.forEach; + +DS.RelationshipChange = function(options) { + this.parentReference = options.parentReference; + this.childReference = options.childReference; + this.firstRecordReference = options.firstRecordReference; + this.firstRecordKind = options.firstRecordKind; + this.firstRecordName = options.firstRecordName; + this.secondRecordReference = options.secondRecordReference; + this.secondRecordKind = options.secondRecordKind; + this.secondRecordName = options.secondRecordName; + this.changeType = options.changeType; + this.store = options.store; + + this.committed = {}; +}; + +DS.RelationshipChangeAdd = function(options){ + DS.RelationshipChange.call(this, options); +}; + +DS.RelationshipChangeRemove = function(options){ + DS.RelationshipChange.call(this, options); +}; + +/** @private */ +DS.RelationshipChange.create = function(options) { + return new DS.RelationshipChange(options); +}; + +/** @private */ +DS.RelationshipChangeAdd.create = function(options) { + return new DS.RelationshipChangeAdd(options); +}; + +/** @private */ +DS.RelationshipChangeRemove.create = function(options) { + return new DS.RelationshipChangeRemove(options); +}; + +DS.OneToManyChange = {}; +DS.OneToNoneChange = {}; +DS.ManyToNoneChange = {}; +DS.OneToOneChange = {}; +DS.ManyToManyChange = {}; + +DS.RelationshipChange._createChange = function(options){ + if(options.changeType === "add"){ + return DS.RelationshipChangeAdd.create(options); + } + if(options.changeType === "remove"){ + return DS.RelationshipChangeRemove.create(options); + } +}; + + +DS.RelationshipChange.determineRelationshipType = function(recordType, knownSide){ + var knownKey = knownSide.key, key, otherKind; + var knownKind = knownSide.kind; + + var inverse = recordType.inverseFor(knownKey); + + if (inverse){ + key = inverse.name; + otherKind = inverse.kind; + } + + if (!inverse){ + return knownKind === "belongsTo" ? "oneToNone" : "manyToNone"; + } + else{ + if(otherKind === "belongsTo"){ + return knownKind === "belongsTo" ? "oneToOne" : "manyToOne"; + } + else{ + return knownKind === "belongsTo" ? "oneToMany" : "manyToMany"; + } + } + +}; + +DS.RelationshipChange.createChange = function(firstRecordReference, secondRecordReference, store, options){ + // Get the type of the child based on the child's client ID + var firstRecordType = firstRecordReference.type, changeType; + changeType = DS.RelationshipChange.determineRelationshipType(firstRecordType, options); + if (changeType === "oneToMany"){ + return DS.OneToManyChange.createChange(firstRecordReference, secondRecordReference, store, options); + } + else if (changeType === "manyToOne"){ + return DS.OneToManyChange.createChange(secondRecordReference, firstRecordReference, store, options); + } + else if (changeType === "oneToNone"){ + return DS.OneToNoneChange.createChange(firstRecordReference, secondRecordReference, store, options); + } + else if (changeType === "manyToNone"){ + return DS.ManyToNoneChange.createChange(firstRecordReference, secondRecordReference, store, options); + } + else if (changeType === "oneToOne"){ + return DS.OneToOneChange.createChange(firstRecordReference, secondRecordReference, store, options); + } + else if (changeType === "manyToMany"){ + return DS.ManyToManyChange.createChange(firstRecordReference, secondRecordReference, store, options); + } +}; + +/** @private */ +DS.OneToNoneChange.createChange = function(childReference, parentReference, store, options) { + var key = options.key; + var change = DS.RelationshipChange._createChange({ + parentReference: parentReference, + childReference: childReference, + firstRecordReference: childReference, + store: store, + changeType: options.changeType, + firstRecordName: key, + firstRecordKind: "belongsTo" + }); + + store.addRelationshipChangeFor(childReference, key, parentReference, null, change); + + return change; +}; + +/** @private */ +DS.ManyToNoneChange.createChange = function(childReference, parentReference, store, options) { + var key = options.key; + var change = DS.RelationshipChange._createChange({ + parentReference: childReference, + childReference: parentReference, + secondRecordReference: childReference, + store: store, + changeType: options.changeType, + secondRecordName: options.key, + secondRecordKind: "hasMany" + }); + + store.addRelationshipChangeFor(childReference, key, parentReference, null, change); + return change; +}; + + +/** @private */ +DS.ManyToManyChange.createChange = function(childReference, parentReference, store, options) { + // If the name of the belongsTo side of the relationship is specified, + // use that + // If the type of the parent is specified, look it up on the child's type + // definition. + var key = options.key; + + var change = DS.RelationshipChange._createChange({ + parentReference: parentReference, + childReference: childReference, + firstRecordReference: childReference, + secondRecordReference: parentReference, + firstRecordKind: "hasMany", + secondRecordKind: "hasMany", + store: store, + changeType: options.changeType, + firstRecordName: key + }); + + store.addRelationshipChangeFor(childReference, key, parentReference, null, change); + + + return change; +}; + +/** @private */ +DS.OneToOneChange.createChange = function(childReference, parentReference, store, options) { + var key; + + // If the name of the belongsTo side of the relationship is specified, + // use that + // If the type of the parent is specified, look it up on the child's type + // definition. + if (options.parentType) { + key = options.parentType.inverseFor(options.key).name; + } else if (options.key) { + key = options.key; + } else { + Ember.assert("You must pass either a parentType or belongsToName option to OneToManyChange.forChildAndParent", false); + } + + var change = DS.RelationshipChange._createChange({ + parentReference: parentReference, + childReference: childReference, + firstRecordReference: childReference, + secondRecordReference: parentReference, + firstRecordKind: "belongsTo", + secondRecordKind: "belongsTo", + store: store, + changeType: options.changeType, + firstRecordName: key + }); + + store.addRelationshipChangeFor(childReference, key, parentReference, null, change); + + + return change; +}; + +DS.OneToOneChange.maintainInvariant = function(options, store, childReference, key){ + if (options.changeType === "add" && store.recordIsMaterialized(childReference)) { + var child = store.recordForReference(childReference); + var oldParent = get(child, key); + if (oldParent){ + var correspondingChange = DS.OneToOneChange.createChange(childReference, oldParent.get('_reference'), store, { + parentType: options.parentType, + hasManyName: options.hasManyName, + changeType: "remove", + key: options.key + }); + store.addRelationshipChangeFor(childReference, key, options.parentReference , null, correspondingChange); + correspondingChange.sync(); + } + } +}; + +/** @private */ +DS.OneToManyChange.createChange = function(childReference, parentReference, store, options) { + var key; + + // If the name of the belongsTo side of the relationship is specified, + // use that + // If the type of the parent is specified, look it up on the child's type + // definition. + if (options.parentType) { + key = options.parentType.inverseFor(options.key).name; + DS.OneToManyChange.maintainInvariant( options, store, childReference, key ); + } else if (options.key) { + key = options.key; + } else { + Ember.assert("You must pass either a parentType or belongsToName option to OneToManyChange.forChildAndParent", false); + } + + var change = DS.RelationshipChange._createChange({ + parentReference: parentReference, + childReference: childReference, + firstRecordReference: childReference, + secondRecordReference: parentReference, + firstRecordKind: "belongsTo", + secondRecordKind: "hasMany", + store: store, + changeType: options.changeType, + firstRecordName: key + }); + + store.addRelationshipChangeFor(childReference, key, parentReference, change.getSecondRecordName(), change); + + + return change; +}; + + +DS.OneToManyChange.maintainInvariant = function(options, store, childReference, key){ + var child = childReference.record; + + if (options.changeType === "add" && child) { + var oldParent = get(child, key); + if (oldParent){ + var correspondingChange = DS.OneToManyChange.createChange(childReference, oldParent.get('_reference'), store, { + parentType: options.parentType, + hasManyName: options.hasManyName, + changeType: "remove", + key: options.key + }); + store.addRelationshipChangeFor(childReference, key, options.parentReference, correspondingChange.getSecondRecordName(), correspondingChange); + correspondingChange.sync(); + } + } +}; + +DS.OneToManyChange.ensureSameTransaction = function(changes){ + var records = Ember.A(); + forEach(changes, function(change){ + records.addObject(change.getSecondRecord()); + records.addObject(change.getFirstRecord()); + }); + + return DS.Transaction.ensureSameTransaction(records); +}; + +DS.RelationshipChange.prototype = { + + getSecondRecordName: function() { + var name = this.secondRecordName, parent; + + if (!name) { + parent = this.secondRecordReference; + if (!parent) { return; } + + var childType = this.firstRecordReference.type; + var inverse = childType.inverseFor(this.firstRecordName); + this.secondRecordName = inverse.name; + } + + return this.secondRecordName; + }, + + /** + Get the name of the relationship on the belongsTo side. + + @return {String} + */ + getFirstRecordName: function() { + var name = this.firstRecordName; + return name; + }, + + /** @private */ + destroy: function() { + var childReference = this.childReference, + belongsToName = this.getFirstRecordName(), + hasManyName = this.getSecondRecordName(), + store = this.store; + + store.removeRelationshipChangeFor(childReference, belongsToName, this.parentReference, hasManyName, this.changeType); + }, + + /** @private */ + getByReference: function(reference) { + // return null or undefined if the original reference was null or undefined + if (!reference) { return reference; } + + if (reference.record) { + return reference.record; + } + }, + + getSecondRecord: function(){ + return this.getByReference(this.secondRecordReference); + }, + + /** @private */ + getFirstRecord: function() { + return this.getByReference(this.firstRecordReference); + }, + + /** + @private + + Make sure that all three parts of the relationship change are part of + the same transaction. If any of the three records is clean and in the + default transaction, and the rest are in a different transaction, move + them all into that transaction. + */ + ensureSameTransaction: function() { + var child = this.getFirstRecord(), + parentRecord = this.getSecondRecord(); + + var transaction = DS.Transaction.ensureSameTransaction([child, parentRecord]); + + this.transaction = transaction; + return transaction; + }, + + callChangeEvents: function(){ + var child = this.getFirstRecord(), + parentRecord = this.getSecondRecord(); + + var dirtySet = new Ember.OrderedSet(); + + // TODO: This implementation causes a race condition in key-value + // stores. The fix involves buffering changes that happen while + // a record is loading. A similar fix is required for other parts + // of ember-data, and should be done as new infrastructure, not + // a one-off hack. [tomhuda] + if (parentRecord && get(parentRecord, 'isLoaded')) { + this.store.recordHasManyDidChange(dirtySet, parentRecord, this); + } + + if (child) { + this.store.recordBelongsToDidChange(dirtySet, child, this); + } + + dirtySet.forEach(function(record) { + record.adapterDidDirty(); + }); + }, + + coalesce: function(){ + var relationshipPairs = this.store.relationshipChangePairsFor(this.firstRecordReference); + forEach(relationshipPairs, function(pair){ + var addedChange = pair["add"]; + var removedChange = pair["remove"]; + if(addedChange && removedChange) { + addedChange.destroy(); + removedChange.destroy(); + } + }); + } +}; + +DS.RelationshipChangeAdd.prototype = Ember.create(DS.RelationshipChange.create({})); +DS.RelationshipChangeRemove.prototype = Ember.create(DS.RelationshipChange.create({})); + +DS.RelationshipChangeAdd.prototype.changeType = "add"; +DS.RelationshipChangeAdd.prototype.sync = function() { + var secondRecordName = this.getSecondRecordName(), + firstRecordName = this.getFirstRecordName(), + firstRecord = this.getFirstRecord(), + secondRecord = this.getSecondRecord(); + + //Ember.assert("You specified a hasMany (" + hasManyName + ") on " + (!belongsToName && (newParent || oldParent || this.lastParent).constructor) + " but did not specify an inverse belongsTo on " + child.constructor, belongsToName); + //Ember.assert("You specified a belongsTo (" + belongsToName + ") on " + child.constructor + " but did not specify an inverse hasMany on " + (!hasManyName && (newParent || oldParent || this.lastParentRecord).constructor), hasManyName); + + this.ensureSameTransaction(); + + this.callChangeEvents(); + + if (secondRecord && firstRecord) { + if(this.secondRecordKind === "belongsTo"){ + secondRecord.suspendRelationshipObservers(function(){ + set(secondRecord, secondRecordName, firstRecord); + }); + + } + else if(this.secondRecordKind === "hasMany"){ + secondRecord.suspendRelationshipObservers(function(){ + get(secondRecord, secondRecordName).addObject(firstRecord); + }); + } + } + + if (firstRecord && secondRecord && get(firstRecord, firstRecordName) !== secondRecord) { + if(this.firstRecordKind === "belongsTo"){ + firstRecord.suspendRelationshipObservers(function(){ + set(firstRecord, firstRecordName, secondRecord); + }); + } + else if(this.firstRecordKind === "hasMany"){ + firstRecord.suspendRelationshipObservers(function(){ + get(firstRecord, firstRecordName).addObject(secondRecord); + }); + } + } + + this.coalesce(); +}; + +DS.RelationshipChangeRemove.prototype.changeType = "remove"; +DS.RelationshipChangeRemove.prototype.sync = function() { + var secondRecordName = this.getSecondRecordName(), + firstRecordName = this.getFirstRecordName(), + firstRecord = this.getFirstRecord(), + secondRecord = this.getSecondRecord(); + + //Ember.assert("You specified a hasMany (" + hasManyName + ") on " + (!belongsToName && (newParent || oldParent || this.lastParent).constructor) + " but did not specify an inverse belongsTo on " + child.constructor, belongsToName); + //Ember.assert("You specified a belongsTo (" + belongsToName + ") on " + child.constructor + " but did not specify an inverse hasMany on " + (!hasManyName && (newParent || oldParent || this.lastParentRecord).constructor), hasManyName); + + this.ensureSameTransaction(firstRecord, secondRecord, secondRecordName, firstRecordName); + + this.callChangeEvents(); + + if (secondRecord && firstRecord) { + if(this.secondRecordKind === "belongsTo"){ + secondRecord.suspendRelationshipObservers(function(){ + set(secondRecord, secondRecordName, null); + }); + } + else if(this.secondRecordKind === "hasMany"){ + secondRecord.suspendRelationshipObservers(function(){ + get(secondRecord, secondRecordName).removeObject(firstRecord); + }); + } + } + + if (firstRecord && get(firstRecord, firstRecordName)) { + if(this.firstRecordKind === "belongsTo"){ + firstRecord.suspendRelationshipObservers(function(){ + set(firstRecord, firstRecordName, null); + }); + } + else if(this.firstRecordKind === "hasMany"){ + firstRecord.suspendRelationshipObservers(function(){ + get(firstRecord, firstRecordName).removeObject(secondRecord); + }); + } + } + + this.coalesce(); +}; + +})(); + + + +(function() { +/** + @module data + @submodule data-changes +*/ })(); @@ -4008,7 +4674,7 @@ DS.attr = function(type, options) { (function() { var get = Ember.get, set = Ember.set, - none = Ember.isNone; + isNone = Ember.isNone; DS.belongsTo = function(type, options) { Ember.assert("The first argument DS.belongsTo must be a model type or string, like DS.belongsTo(App.Person)", !!type && (typeof type === 'string' || DS.Model.detect(type))); @@ -4018,26 +4684,43 @@ DS.belongsTo = function(type, options) { var meta = { type: type, isRelationship: true, options: options, kind: 'belongsTo' }; return Ember.computed(function(key, value) { + if (typeof type === 'string') { + type = get(this, type, false) || get(Ember.lookup, type); + } + if (arguments.length === 2) { + Ember.assert("You can only add a record of " + type.toString() + " to this relationship", !value || type.detectInstance(value)); return value === undefined ? null : value; } var data = get(this, 'data').belongsTo, - store = get(this, 'store'), id; + store = get(this, 'store'), belongsTo; - if (typeof type === 'string') { - type = get(this, type, false) || get(Ember.lookup, type); - } + belongsTo = data[key]; - id = data[key]; + // TODO (tomdale) The value of the belongsTo in the data hash can be + // one of: + // 1. null/undefined + // 2. a record reference + // 3. a tuple returned by the serializer's polymorphism code + // + // We should really normalize #3 to be the same as #2 to reduce the + // complexity here. - if(!id) { + if (isNone(belongsTo)) { return null; - } else if (typeof id === 'object') { - return store.recordForReference(id); - } else { - return store.find(type, id); } + + // The data has been normalized to a record reference, so + // just ask the store for the record for that reference, + // materializing it if necessary. + if (belongsTo.clientId) { + return store.recordForReference(belongsTo); + } + + // The data has been normalized into a type/id pair by the + // serializer's polymorphism code. + return store.findById(belongsTo.type, belongsTo.id); }).property('data').meta(meta); }; @@ -4086,7 +4769,7 @@ DS.Model.reopen({ (function() { -var get = Ember.get, set = Ember.set; +var get = Ember.get, set = Ember.set, forEach = Ember.EnumerableUtils.forEach; var hasRelationship = function(type, options) { options = options || {}; @@ -4101,10 +4784,14 @@ var hasRelationship = function(type, options) { type = get(this, type, false) || get(Ember.lookup, type); } + //ids can be references or opaque token + //(e.g. `{url: '/relationship'}`) that will be passed to the adapter ids = data[key]; + relationship = store.findMany(type, ids, this, meta); set(relationship, 'owner', this); set(relationship, 'name', key); + set(relationship, 'isPolymorphic', options.polymorphic); return relationship; }).property().meta(meta); @@ -4115,6 +4802,40 @@ DS.hasMany = function(type, options) { return hasRelationship(type, options); }; +function clearUnmaterializedHasMany(record, relationship) { + var data = get(record, 'data').hasMany; + + var references = data[relationship.key]; + + if (!references) { return; } + + var inverse = record.constructor.inverseFor(relationship.key); + + if (inverse) { + forEach(references, function(reference) { + var childRecord; + + if (childRecord = reference.record) { + record.suspendRelationshipObservers(function() { + set(childRecord, inverse.name, null); + }); + } + }); + } +} + +DS.Model.reopen({ + clearHasMany: function(relationship) { + var hasMany = this.cacheFor(relationship.name); + + if (hasMany) { + hasMany.clear(); + } else { + clearUnmaterializedHasMany(this, relationship); + } + } +}); + })(); @@ -4208,6 +4929,53 @@ DS.Model.reopenClass({ return relationship && relationship.type; }, + inverseFor: function(name) { + var inverseType = this.typeForRelationship(name); + + if (!inverseType) { return null; } + + var options = this.metaForProperty(name).options; + var inverseName, inverseKind; + + if (options.inverse) { + inverseName = options.inverse; + inverseKind = Ember.get(inverseType, 'relationshipsByName').get(inverseName).kind; + } else { + var possibleRelationships = findPossibleInverses(this, inverseType); + + if (possibleRelationships.length === 0) { return null; } + + Ember.assert("You defined the '" + name + "' relationship on " + this + ", but multiple possible inverse relationships of type " + this + " were found on " + inverseType + ".", possibleRelationships.length === 1); + + inverseName = possibleRelationships[0].name; + inverseKind = possibleRelationships[0].kind; + } + + function findPossibleInverses(type, inverseType, possibleRelationships) { + possibleRelationships = possibleRelationships || []; + + var relationshipMap = get(inverseType, 'relationships'); + if (!relationshipMap) { return; } + + var relationships = relationshipMap.get(type); + if (relationships) { + possibleRelationships.push.apply(possibleRelationships, relationshipMap.get(type)); + } + + if (type.superclass) { + findPossibleInverses(type.superclass, inverseType, possibleRelationships); + } + + return possibleRelationships; + } + + return { + type: inverseType, + name: inverseName, + kind: inverseKind + }; + }, + /** The model's relationships as a map, keyed on the type of the relationship. The value of each entry is an array containing a descriptor @@ -4226,7 +4994,7 @@ DS.Model.reopenClass({ relationships, like this: var relationships = Ember.get(App.Blog, 'relationships'); - associatons.get(App.User); + relationships.get(App.User); //=> [ { name: 'users', kind: 'hasMany' }, // { name: 'owner', kind: 'belongsTo' } ] relationships.get(App.Post); @@ -4330,6 +5098,8 @@ DS.Model.reopenClass({ type = get(this, type, false) || get(Ember.lookup, type); } + Ember.assert("You specified a hasMany (" + meta.type + ") on " + meta.parentType + " but " + meta.type + " was not found.", type); + if (!types.contains(type)) { Ember.assert("Trying to sideload " + name + " on " + this.toString() + " but the type doesn't exist.", !!type); types.push(type); @@ -4416,7 +5186,7 @@ DS.Model.reopenClass({ @readOnly */ fields: Ember.computed(function() { - var map = Ember.Map.create(), type; + var map = Ember.Map.create(); this.eachComputedProperty(function(name, meta) { if (meta.isRelationship) { @@ -4473,69 +5243,15 @@ DS.Model.reopen({ } }); -/** - @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]; -}; +(function() { /** - @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} + @module data + @submodule data-relationships */ -DS._inverseTypeFor = function(modelType, relationshipName) { - var relationships = get(modelType, 'relationshipsByName'), - relationship = relationships.get(relationshipName); - - if (relationship) { return relationship.type; } -}; })(); @@ -4543,642 +5259,206 @@ DS._inverseTypeFor = function(modelType, relationshipName) { (function() { var get = Ember.get, set = Ember.set; +var once = Ember.run.once; 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); - } +DS.RecordArrayManager = Ember.Object.extend({ + init: function() { + this.filteredRecordArrays = Ember.MapWithDefault.create({ + defaultValue: function() { return []; } + }); - var change = DS.RelationshipChange._createChange({ - parentReference: parentReference, - childReference: childReference, - firstRecordReference: childReference, - secondRecordReference: parentReference, - firstRecordKind: "belongsTo", - secondRecordKind: "hasMany", - store: store, - changeType: options.changeType, - firstRecordName: key - }); + this.changedReferences = []; + }, - store.addRelationshipChangeFor(childReference, key, parentReference, null, change); + referenceDidChange: function(reference) { + this.changedReferences.push(reference); + once(this, this.updateRecordArrays); + }, + recordArraysForReference: function(reference) { + reference.recordArrays = reference.recordArrays || Ember.OrderedSet.create(); + return reference.recordArrays; + }, - return change; -}; + /** + @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. -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(); - } - } -}; + It updates all filters that a record belongs to. -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; - }); -}; + To avoid thrashing, it only runs once per run loop per record. -DS.RelationshipChange.prototype = { + @param {Class} type + @param {Number|String} clientId + */ + updateRecordArrays: function() { + forEach(this.changedReferences, function(reference) { + var type = reference.type, + recordArrays = this.filteredRecordArrays.get(type), + filter; + + forEach(recordArrays, function(array) { + filter = get(array, 'filterFunction'); + this.updateRecordArray(array, filter, type, reference); + }, this); - getSecondRecordName: function() { - var name = this.secondRecordName, store = this.store, parent; + // loop through all manyArrays containing an unloaded copy of this + // clientId and notify them that the record was loaded. + var manyArrays = reference.loadingRecordArrays; - if (!name) { - parent = this.secondRecordReference; - if (!parent) { return; } + if (manyArrays) { + for (var i=0, l=manyArrays.length; i<l; i++) { + manyArrays[i].loadedRecord(); + } - var childType = this.firstRecordReference.type; - var inverseType = DS._inverseTypeFor(childType, this.firstRecordName); - name = inverseHasManyName(inverseType, childType, this.firstRecordName); - this.secondRecordName = name; - } + reference.loadingRecordArrays = []; + } + }, this); - return name; + this.changedReferences = []; }, /** - 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 - /** @private */ - destroy: function() { - var childReference = this.childReference, - belongsToName = this.getFirstRecordName(), - hasManyName = this.getSecondRecordName(), - store = this.store, - child, oldParent, newParent, lastParent, transaction; + Update an individual filter. - store.removeRelationshipChangeFor(childReference, belongsToName, this.parentReference, hasManyName, this.changeType); + @param {DS.FilteredRecordArray} array + @param {Function} filter + @param {Class} type + @param {Number|String} clientId + */ + updateRecordArray: function(array, filter, type, reference) { + var shouldBeInArray, record; - if (transaction = this.transaction) { - transaction.relationshipBecameClean(this); + if (!filter) { + shouldBeInArray = true; + } else { + record = this.store.recordForReference(reference); + shouldBeInArray = filter(record); } - }, - /** @private */ - getByReference: function(reference) { - var store = this.store; - - // return null or undefined if the original reference was null or undefined - if (!reference) { return reference; } + var recordArrays = this.recordArraysForReference(reference); - if (store.recordIsMaterialized(reference)) { - return store.recordForReference(reference); + if (shouldBeInArray) { + recordArrays.add(array); + array.addReference(reference); + } else if (!shouldBeInArray) { + recordArrays.remove(array); + array.removeReference(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); - } + When a record is deleted, it is removed from all its + record arrays. - if (child) { - this.store.recordBelongsToDidChange(dirtySet, child, this); - } + @param {DS.Model} record + */ + remove: function(record) { + var reference = get(record, '_reference'); + var recordArrays = reference.recordArrays || []; - dirtySet.forEach(function(record) { - record.adapterDidDirty(); + recordArrays.forEach(function(array) { + array.removeReference(reference); }); }, - 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(); -}; + /** + @private -DS.RelationshipChangeRemove.prototype.changeType = "remove"; -DS.RelationshipChangeRemove.prototype.sync = function() { - var secondRecordName = this.getSecondRecordName(), - firstRecordName = this.getFirstRecordName(), - firstRecord = this.getFirstRecord(), - secondRecord = this.getSecondRecord(); + This method is invoked if the `filterFunction` property is + changed on a `DS.FilteredRecordArray`. - //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); + It essentially re-runs the filter from scratch. This same + method is invoked when the filter is created in th first place. + */ + updateFilter: function(array, type, filter) { + var typeMap = this.store.typeMapFor(type), + references = typeMap.references, + reference, data, shouldFilter, record; - var transaction = this.ensureSameTransaction(firstRecord, secondRecord, secondRecordName, firstRecordName); - transaction.relationshipBecameDirty(this); + for (var i=0, l=references.length; i<l; i++) { + reference = references[i]; + shouldFilter = false; - this.callChangeEvents(); + data = reference.data; - 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 (typeof data === 'object') { + if (record = reference.record) { + if (!get(record, 'isDeleted')) { shouldFilter = true; } + } else { + shouldFilter = true; + } - 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); - }); + if (shouldFilter) { + this.updateRecordArray(array, filter, type, reference); + } + } } - } - - 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: + /** + @private - App.Store = DS.Store.extend({ - adapter: 'App.MyCustomAdapter' - }); + Create a `DS.ManyArray` for a type and list of record references, and index + the `ManyArray` under each reference. This allows us to efficiently remove + records from `ManyArray`s when they are deleted. - App.PostsController = Ember.ArrayController.extend({ - // ... - }); + @param {Class} type + @param {Array} references - 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. + @return {DS.ManyArray} + */ + createManyArray: function(type, references) { + var manyArray = DS.ManyArray.create({ + type: type, + content: references, + store: this.store + }); - 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. -*/ + references.forEach(function(reference) { + var arrays = this.recordArraysForReference(reference); + arrays.add(manyArray); + }, this); -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()); - } - } - }); + return manyArray; + }, - 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", + /** + @private - initialize: function(container, application) { - application.register('store:main', application.Store); + 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. - // Eagerly generate the store so defaultStore is populated. - // TODO: Do this in a finisher hook - container.lookup('store:main'); - } - }); + @param {DS.RecordArray} array + @param {Class} type + @param {Function} filter + */ + registerFilteredRecordArray: function(array, type, filter) { + var recordArrays = this.filteredRecordArrays.get(type); + recordArrays.push(array); - Application.initializer({ - name: "injectStore", + this.updateFilter(array, type, filter); + }, - initialize: function(container, application) { - application.inject('controller', 'store', 'store:main'); - application.inject('route', 'store', 'store:main'); - } - }); + // Internally, we maintain a map of all unloaded IDs requested by + // a ManyArray. As the adapter loads data into the store, the + // store notifies any interested ManyArrays. When the ManyArray's + // total number of loading records drops to zero, it becomes + // `isLoaded` and fires a `didLoad` event. + registerWaitingRecordArray: function(array, reference) { + var loadingRecordArrays = reference.loadingRecordArrays || []; + loadingRecordArrays.push(array); + reference.loadingRecordArrays = loadingRecordArrays; } }); @@ -5388,36 +5668,50 @@ function mustImplement(name) { relationship types are added. However, it will remain backwards-compatible, so the mere existence of new features should not break existing adapters. + + @module data + @submodule data-serializer + @main data-serializer + + @class Serializer + @namespace DS + @extends Ember.Object + @constructor */ + DS.Serializer = Ember.Object.extend({ init: function() { this.mappings = Ember.Map.create(); + this.aliases = Ember.Map.create(); this.configurations = Ember.Map.create(); this.globalConfigurations = {}; }, extract: mustImplement('extract'), extractMany: mustImplement('extractMany'), + extractId: mustImplement('extractId'), + extractAttribute: mustImplement('extractAttribute'), + extractHasMany: mustImplement('extractHasMany'), + extractBelongsTo: mustImplement('extractBelongsTo'), - extractRecordRepresentation: function(loader, type, json, shouldSideload) { - var mapping = this.mappingForType(type); - var embeddedData, prematerialized = {}, reference; + extractRecordRepresentation: function(loader, type, data, shouldSideload) { + var prematerialized = {}, reference; if (shouldSideload) { - reference = loader.sideload(type, json); + reference = loader.sideload(type, data); } else { - reference = loader.load(type, json); + reference = loader.load(type, data); } this.eachEmbeddedHasMany(type, function(name, relationship) { - var embeddedData = json[this.keyFor(relationship)]; + var embeddedData = this.extractEmbeddedData(data, this.keyFor(relationship)); if (!isNone(embeddedData)) { this.extractEmbeddedHasMany(loader, relationship, embeddedData, reference, prematerialized); } }, this); this.eachEmbeddedBelongsTo(type, function(name, relationship) { - var embeddedData = json[this.keyFor(relationship)]; + var embeddedData = this.extractEmbeddedData(data, this.keyFor(relationship)); if (!isNone(embeddedData)) { this.extractEmbeddedBelongsTo(loader, relationship, embeddedData, reference, prematerialized); } @@ -5432,7 +5726,8 @@ DS.Serializer = Ember.Object.extend({ var references = map.call(array, function(item) { if (!item) { return; } - var reference = this.extractRecordRepresentation(loader, relationship.type, item, true); + var foundType = this.extractEmbeddedType(relationship, item), + reference = this.extractRecordRepresentation(loader, foundType, item, true); // If the embedded record should also be saved back when serializing the parent, // make sure we set its parent since it will not have an ID. @@ -5441,6 +5736,16 @@ DS.Serializer = Ember.Object.extend({ reference.parent = parent; } + // If the embedded children have an inverse belongs-to, set the + // inverse to the current record in their prematerialized data. + var parentType = relationship.parentType, + inverse = parentType.inverseFor(relationship.key); + + if (inverse) { + var inverseName = inverse.name; + reference.prematerialized[inverseName] = parent; + } + return reference; }, this); @@ -5448,7 +5753,8 @@ DS.Serializer = Ember.Object.extend({ }, extractEmbeddedBelongsTo: function(loader, relationship, data, parent, prematerialized) { - var reference = this.extractRecordRepresentation(loader, relationship.type, data, true); + var foundType = this.extractEmbeddedType(relationship, data), + reference = this.extractRecordRepresentation(loader, foundType, data, true); prematerialized[relationship.key] = reference; // If the embedded record should also be saved back when serializing the parent, @@ -5459,6 +5765,34 @@ DS.Serializer = Ember.Object.extend({ } }, + /** + A hook you can use to customize how the record's type is extracted from + the serialized data. + + The `extractEmbeddedType` hook is called with: + + * the relationship + * the serialized representation of the record + + By default, it returns the type of the relationship. + + @method extractEmbeddedType + @param {Object} relationship an object representing the relationship + @param {any} data the serialized representation of the record + */ + extractEmbeddedType: function(relationship, data) { + return relationship.type; + }, + + /** + A hook you need to implement in order to extract + the data associated with an embedded record. + + @param {any} data the serialized representation of the record + @param {String} key the key that represents the embedded record + */ + extractEmbeddedData: mustImplement(), + //....................... //. SERIALIZATION HOOKS //....................... @@ -5474,11 +5808,13 @@ DS.Serializer = Ember.Object.extend({ * If the option hash contains `includeId`, add the record's ID to the serialized form. By default, `serialize` calls `addId` if appropriate. + * If the option hash contains `includeType`, add the record's type to the serialized form. * Add the record's attributes to the serialized form. By default, `serialize` calls `addAttributes`. * Add the record's relationships to the serialized form. By default, `serialize` calls `addRelationships`. + @method serialize @param {DS.Model} record the record to serialize @param {Object} [options] a hash of options @returns {any} the serialized form of the record @@ -5494,6 +5830,10 @@ DS.Serializer = Ember.Object.extend({ } } + if (options.includeType) { + this.addType(serialized, record.constructor); + } + this.addAttributes(serialized, record); this.addRelationships(serialized, record); @@ -5506,6 +5846,7 @@ DS.Serializer = Ember.Object.extend({ Given an attribute type and value, convert the value into the serialized form using the transform registered for that type. + @method serializeValue @param {any} value the value to convert to the serialized form @param {String} attributeType the registered type (e.g. `string` or `boolean`) @@ -5544,8 +5885,9 @@ DS.Serializer = Ember.Object.extend({ 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. + custom transforms, you should rarely need to override this hook. + @method addAttributes @param {any} data the serialized representation that is being built @param {DS.Model} record the record to serialize */ @@ -5559,11 +5901,12 @@ DS.Serializer = Ember.Object.extend({ A hook you can use to customize how the key/value pair is added to the serialized data. + @method addAttribute @param {any} serialized the serialized form being built @param {String} key the key to add to the serialized data @param {any} value the value to add to the serialized data */ - addAttribute: Ember.K, + addAttribute: mustImplement('addAttribute'), /** A hook you can use to customize how the record's id is added to @@ -5576,17 +5919,44 @@ DS.Serializer = Ember.Object.extend({ `primaryKey` hook into consideration) * the serialized id (after calling the `serializeId` hook) + @method addId @param {any} data the serialized representation that is being built @param {String} key the resolved primary key @param {id} id the serialized id */ - addId: Ember.K, + addId: mustImplement('addId'), + + /** + A hook you can use to customize how the record's type is added to + the serialized data. + + The `addType` hook is called with: + + * the serialized representation being built + * the serialized id (after calling the `serializeId` hook) + + @method addType + @param {any} data the serialized representation that is being built + @param {DS.Model subclass} type the type of the record + */ + addType: Ember.K, + + /** + Creates an empty hash that will be filled in by the hooks called from the + `serialize()` method. + + @method createSerializedForm + @return {Object} + */ + createSerializedForm: function() { + return {}; + }, /** A hook you can use to change how relationships are added to the serialized representation of a record. - By default, `addAttributes` loops over all of the relationships of the + By default, `addRelationships` loops over all of the relationships of the passed record, maps the relationship names to the key for the serialized form, and then invokes the public `addBelongsTo` and `addHasMany` hooks. @@ -5594,6 +5964,7 @@ DS.Serializer = Ember.Object.extend({ `addHasMany`, and register mappings, you should rarely need to override this hook. + @method addRelationships @param {any} data the serialized representation that is being built @param {DS.Model} record the record to serialize */ @@ -5624,12 +5995,13 @@ DS.Serializer = Ember.Object.extend({ Additional properties may be added in the future. + @method addBelongsTo @param {any} data the serialized representation that is being built @param {DS.Model} record the record to serialize @param {String} key the key for the serialized object @param {Object} relationship an object representing the relationship */ - addBelongsTo: Ember.K, + addBelongsTo: mustImplement('addBelongsTo'), /** A hook you can use to add a `hasMany` relationship to the @@ -5649,12 +6021,13 @@ DS.Serializer = Ember.Object.extend({ Additional properties may be added in the future. + @method addHasMany @param {any} data the serialized representation that is being built @param {DS.Model} record the record to serialize @param {String} key the key for the serialized object @param {Object} relationship an object representing the relationship */ - addHasMany: Ember.K, + addHasMany: mustImplement('addHasMany'), /** NAMING CONVENTIONS @@ -5691,6 +6064,7 @@ DS.Serializer = Ember.Object.extend({ }); ``` + @method keyForAttributeName @param {DS.Model subclass} type the type of the record with the attribute name `name` @param {String} name the attribute name to convert into a key @@ -5736,6 +6110,7 @@ DS.Serializer = Ember.Object.extend({ }); ``` + @method primaryKey @param {DS.Model subclass} type @returns {String} the primary key for the type */ @@ -5766,6 +6141,7 @@ DS.Serializer = Ember.Object.extend({ }); ``` + @method keyForBelongsTo @param {DS.Model subclass} type the type of the record with the `belongsTo` relationship. @param {String} name the relationship name to convert into a key @@ -5807,6 +6183,7 @@ DS.Serializer = Ember.Object.extend({ }); ``` + @method keyForHasMany @param {DS.Model subclass} type the type of the record with the `belongsTo` relationship. @param {String} name the relationship name to convert into a key @@ -5839,7 +6216,7 @@ DS.Serializer = Ember.Object.extend({ 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); + Ember.assert("You tried to use an attribute type (" + attributeType + ") that has not been registered", transform); return transform.deserialize(value); }, @@ -5860,48 +6237,88 @@ DS.Serializer = Ember.Object.extend({ record.materializeAttribute(attributeName, value); }, - materializeRelationships: function(record, hash, prematerialized) { + materializeRelationships: function(record, serialized, prematerialized) { record.eachRelationship(function(name, relationship) { if (relationship.kind === 'hasMany') { if (prematerialized && prematerialized.hasOwnProperty(name)) { - record.materializeHasMany(name, prematerialized[name]); + var tuplesOrReferencesOrOpaque = this._convertPrematerializedHasMany(relationship.type, prematerialized[name]); + record.materializeHasMany(name, tuplesOrReferencesOrOpaque); } else { - this.materializeHasMany(name, record, hash, relationship, prematerialized); + this.materializeHasMany(name, record, serialized, relationship, prematerialized); } } else if (relationship.kind === 'belongsTo') { if (prematerialized && prematerialized.hasOwnProperty(name)) { - record.materializeBelongsTo(name, prematerialized[name]); + var tupleOrReference = this._convertTuple(relationship.type, prematerialized[name]); + record.materializeBelongsTo(name, tupleOrReference); } else { - this.materializeBelongsTo(name, record, hash, relationship, prematerialized); + this.materializeBelongsTo(name, record, serialized, 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)); + var type = record.constructor, + key = this._keyForHasMany(type, relationship.key), + idsOrTuples = this.extractHasMany(type, hash, key), + tuples = idsOrTuples; + + if(idsOrTuples && Ember.isArray(idsOrTuples)) { + tuples = this._convertTuples(relationship.type, idsOrTuples); + } + + record.materializeHasMany(name, tuples); }, materializeBelongsTo: function(name, record, hash, relationship) { - var key = this._keyForBelongsTo(record.constructor, relationship.key); - record.materializeBelongsTo(name, this.extractBelongsTo(record.constructor, hash, key)); - }, + var type = record.constructor, + key = this._keyForBelongsTo(type, relationship.key), + idOrTuple, + tuple = null; + + if(relationship.options && relationship.options.polymorphic) { + idOrTuple = this.extractBelongsToPolymorphic(type, hash, key); + } else { + idOrTuple = this.extractBelongsTo(type, hash, key); + } - _extractEmbeddedRelationship: function(type, hash, name, relationshipType) { - var key = this['_keyFor' + relationshipType](type, name); + if(!isNone(idOrTuple)) { + tuple = this._convertTuple(relationship.type, idOrTuple); + } - if (this.embeddedType(type, name)) { - return this['extractEmbedded' + relationshipType](type, hash, key); + record.materializeBelongsTo(name, tuple); + }, + + _convertPrematerializedHasMany: function(type, prematerializedHasMany) { + var tuplesOrReferencesOrOpaque; + if( typeof prematerializedHasMany === 'string' ) { + tuplesOrReferencesOrOpaque = prematerializedHasMany; + } else { + tuplesOrReferencesOrOpaque = this._convertTuples(type, prematerializedHasMany); } + return tuplesOrReferencesOrOpaque; }, - _extractEmbeddedBelongsTo: function(type, hash, name) { - return this._extractEmbeddedRelationship(type, hash, name, 'BelongsTo'); + _convertTuples: function(type, idsOrTuples) { + return map.call(idsOrTuples, function(idOrTuple) { + return this._convertTuple(type, idOrTuple); + }, this); }, - _extractEmbeddedHasMany: function(type, hash, name) { - return this._extractEmbeddedRelationship(type, hash, name, 'HasMany'); + _convertTuple: function(type, idOrTuple) { + var foundType; + + if (typeof idOrTuple === 'object') { + if (DS.Model.detect(idOrTuple.type)) { + return idOrTuple; + } else { + foundType = this.typeFromAlias(idOrTuple.type); + Ember.assert("Unable to resolve type " + idOrTuple.type + ". You may need to configure your serializer aliases.", !!foundType); + + return {id: idOrTuple.id, type: foundType}; + } + } + return {id: idOrTuple, type: type}; }, /** @@ -5914,6 +6331,7 @@ DS.Serializer = Ember.Object.extend({ method will return the configured value. Otherwise, it will call the public `primaryKey` hook. + @method _primaryKey @param {DS.Model subclass} type @returns {String} the primary key for the type */ @@ -5944,6 +6362,7 @@ DS.Serializer = Ember.Object.extend({ 4. Invoke the public `addAttribute` hook with the hash, key, and transformed value. + @method _addAttribute @param {any} data the serialized representation being built @param {DS.Model} record the record to serialize @param {String} attributeName the name of the attribute on the record @@ -5966,6 +6385,7 @@ DS.Serializer = Ember.Object.extend({ It then invokes the public `addId` hook with the primary key and the serialized id. + @method _addId @param {any} data the serialized representation that is being built @param {Ember.Model subclass} type @param {any} id the materialized id from the record @@ -5983,6 +6403,7 @@ DS.Serializer = Ember.Object.extend({ an attribute name. It first checks for any mappings before calling the public hook `keyForAttributeName`. + @method _keyForAttributeName @param {DS.Model subclass} type the type of the record with the attribute name `name` @param {String} name the attribute name to convert into a key @@ -6000,6 +6421,7 @@ DS.Serializer = Ember.Object.extend({ a belongsTo relationship. It first checks for any mappings before calling the public hook `keyForBelongsTo`. + @method _keyForBelongsTo @param {DS.Model subclass} type the type of the record with the `belongsTo` relationship. @param {String} name the relationship name to convert into a key @@ -6029,6 +6451,7 @@ DS.Serializer = Ember.Object.extend({ a hasMany relationship. It first checks for any mappings before calling the public hook `keyForHasMany`. + @method _keyForHasMany @param {DS.Model subclass} type the type of the record with the `hasMany` relationship. @param {String} name the relationship name to convert into a key @@ -6044,6 +6467,7 @@ DS.Serializer = Ember.Object.extend({ This method converts the relationship name to a key for serialization, and then invokes the public `addBelongsTo` hook. + @method _addBelongsTo @param {any} data the serialized representation that is being built @param {DS.Model} record the record to serialize @param {String} name the relationship name @@ -6060,6 +6484,7 @@ DS.Serializer = Ember.Object.extend({ This method converts the relationship name to a key for serialization, and then invokes the public `addHasMany` hook. + @method _addHasMany @param {any} data the serialized representation that is being built @param {DS.Model} record the record to serialize @param {String} name the relationship name @@ -6080,6 +6505,7 @@ DS.Serializer = Ember.Object.extend({ If a mapping is found, and the mapping has a key defined, use that instead of invoking the hook. + @method _keyFromMappingOrHook @param {String} publicMethod the public hook to invoke if a mapping is not found (e.g. `keyForAttributeName`) @param {DS.Model subclass} type the type of the record with @@ -6107,11 +6533,11 @@ DS.Serializer = Ember.Object.extend({ registerEnumTransform: function(type, objects) { var transform = { - deserialize: function(deserialized) { - return Ember.A(objects).objectAt(deserialized); + deserialize: function(serialized) { + return Ember.A(objects).objectAt(serialized); }, - serialize: function(serialized) { - return Ember.EnumerableUtils.indexOf(objects, serialized); + serialize: function(deserialized) { + return Ember.EnumerableUtils.indexOf(objects, deserialized); }, values: objects }; @@ -6132,12 +6558,25 @@ DS.Serializer = Ember.Object.extend({ return; } - var config = Ember.create(this.globalConfigurations); + var config, alias; + + if (configuration.alias) { + alias = configuration.alias; + this.aliases.set(alias, type); + delete configuration.alias; + } + + config = Ember.create(this.globalConfigurations); Ember.merge(config, configuration); this.configurations.set(type, config); }, + typeFromAlias: function(alias) { + this._completeAliases(); + return this.aliases.get(alias); + }, + mappingForType: function(type) { this._reifyMappings(); return this.mappings.get(type) || {}; @@ -6148,6 +6587,59 @@ DS.Serializer = Ember.Object.extend({ return this.configurations.get(type) || this.globalConfigurations; }, + _completeAliases: function() { + this._pluralizeAliases(); + this._reifyAliases(); + }, + + _pluralizeAliases: function() { + if (this._didPluralizeAliases) { return; } + + var aliases = this.aliases, + sideloadMapping = this.aliases.sideloadMapping, + plural, + self = this; + + aliases.forEach(function(key, type) { + plural = self.pluralize(key); + Ember.assert("The '" + key + "' alias has already been defined", !aliases.get(plural)); + aliases.set(plural, type); + }); + + // This map is only for backward compatibility with the `sideloadAs` option. + if (sideloadMapping) { + sideloadMapping.forEach(function(key, type) { + Ember.assert("The '" + key + "' alias has already been defined", !aliases.get(key) || (aliases.get(key)===type) ); + aliases.set(key, type); + }); + delete this.aliases.sideloadMapping; + } + + this._didPluralizeAliases = true; + }, + + _reifyAliases: function() { + if (this._didReifyAliases) { return; } + + var aliases = this.aliases, + reifiedAliases = Ember.Map.create(), + foundType; + + aliases.forEach(function(key, type) { + if (typeof type === 'string') { + foundType = Ember.get(Ember.lookup, type); + Ember.assert("Could not find model at path " + key, type); + + reifiedAliases.set(key, foundType); + } else { + reifiedAliases.set(key, type); + } + }); + + this.aliases = reifiedAliases; + this._didReifyAliases = true; + }, + _reifyMappings: function() { if (this._didReifyMappings) { return; } @@ -6216,8 +6708,6 @@ DS.Serializer = Ember.Object.extend({ }, 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); } @@ -6225,8 +6715,6 @@ DS.Serializer = Ember.Object.extend({ }, 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++) { @@ -6253,6 +6741,33 @@ DS.Serializer = Ember.Object.extend({ } } }, this); + }, + + // HELPERS + + // define a plurals hash in your subclass to define + // special-case pluralization + pluralize: function(name) { + var plurals = this.configurations.get('plurals'); + return (plurals && plurals[name]) || name + "s"; + }, + + // use the same plurals hash to determine + // special-case singularization + singularize: function(name) { + var plurals = this.configurations.get('plurals'); + if (plurals) { + for (var i in plurals) { + if (plurals[i] === name) { + return i; + } + } + } + if (name.lastIndexOf('s') === name.length - 1) { + return name.substring(0, name.length - 1); + } else { + return name; + } } }); @@ -6262,29 +6777,38 @@ DS.Serializer = Ember.Object.extend({ (function() { -var none = Ember.isNone; +var isNone = Ember.isNone, isEmpty = Ember.isEmpty; + +/** + @module data + @submodule data-transforms +*/ /** DS.Transforms is a hash of transforms used by DS.Serializer. + + @class JSONTransforms + @static + @namespace DS */ DS.JSONTransforms = { string: { deserialize: function(serialized) { - return none(serialized) ? null : String(serialized); + return isNone(serialized) ? null : String(serialized); }, serialize: function(deserialized) { - return none(deserialized) ? null : String(deserialized); + return isNone(deserialized) ? null : String(deserialized); } }, number: { deserialize: function(serialized) { - return none(serialized) ? null : Number(serialized); + return isEmpty(serialized) ? null : Number(serialized); }, serialize: function(deserialized) { - return none(deserialized) ? null : Number(deserialized); + return isEmpty(deserialized) ? null : Number(deserialized); } }, @@ -6313,22 +6837,11 @@ DS.JSONTransforms = { 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; + if (type === "string") { + return new Date(Ember.Date.parse(serialized)); + } else if (type === "number") { + return new Date(serialized); } else if (serialized === null || serialized === undefined) { // if the value is not present in the data, // return undefined, not null. @@ -6374,9 +6887,19 @@ DS.JSONTransforms = { (function() { -var get = Ember.get, set = Ember.set; +/** + @module data + @submodule data-serializers +*/ -var generatedId = 0; +/** + @class JSONSerializer + @constructor + @namespace DS + @extends DS.Serializer +*/ + +var get = Ember.get, set = Ember.set; DS.JSONSerializer = DS.Serializer.extend({ init: function() { @@ -6387,6 +6910,7 @@ DS.JSONSerializer = DS.Serializer.extend({ } this.sideloadMapping = Ember.Map.create(); + this.metadataMapping = Ember.Map.create(); this.configure({ meta: 'meta', @@ -6395,21 +6919,23 @@ DS.JSONSerializer = DS.Serializer.extend({ }, configure: function(type, configuration) { + var key; + if (type && !configuration) { + for(key in type){ + this.metadataMapping.set(get(type, key), key); + } + return this._super(type); } - var sideloadAs = configuration.sideloadAs; + var sideloadAs = configuration.sideloadAs, + sideloadMapping; 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; - + sideloadMapping = this.aliases.sideloadMapping || Ember.Map.create(); + sideloadMapping.set(sideloadAs, type); + this.aliases.sideloadMapping = sideloadMapping; delete configuration.sideloadAs; } @@ -6432,18 +6958,6 @@ DS.JSONSerializer = DS.Serializer.extend({ 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]; @@ -6463,6 +6977,10 @@ DS.JSONSerializer = DS.Serializer.extend({ } }, + extractEmbeddedData: function(hash, key) { + return hash[key]; + }, + extractHasMany: function(type, hash, key) { return hash[key]; }, @@ -6471,24 +6989,53 @@ DS.JSONSerializer = DS.Serializer.extend({ return hash[key]; }, + extractBelongsToPolymorphic: function(type, hash, key) { + var keyForId = this.keyForPolymorphicId(key), + keyForType, + id = hash[keyForId]; + + if (id) { + keyForType = this.keyForPolymorphicType(key); + return {id: id, type: hash[keyForType]}; + } + + return null; + }, + addBelongsTo: function(hash, record, key, relationship) { var type = record.constructor, name = relationship.key, value = null, - embeddedChild; + includeType = (relationship.options && relationship.options.polymorphic), + embeddedChild, + child, + id; if (this.embeddedType(type, name)) { if (embeddedChild = get(record, name)) { - value = this.serialize(embeddedChild, { includeId: true }); + value = this.serialize(embeddedChild, { includeId: true, includeType: includeType }); } hash[key] = value; } else { - var id = get(record, relationship.key+'.id'); - if (!Ember.isNone(id)) { hash[key] = id; } + child = get(record, relationship.key); + id = get(child, 'id'); + + if (relationship.options && relationship.options.polymorphic && !Ember.isNone(id)) { + this.addBelongsToPolymorphic(hash, key, id, child.constructor); + } else { + hash[key] = id === undefined ? null : this.serializeId(id); + } } }, + addBelongsToPolymorphic: function(hash, key, id, type) { + var keyForId = this.keyForPolymorphicId(key), + keyForType = this.keyForPolymorphicType(key); + hash[keyForId] = id; + hash[keyForType] = this.rootForType(type); + }, + /** Adds a has-many relationship to the JSON hash being built. @@ -6504,10 +7051,12 @@ DS.JSONSerializer = DS.Serializer.extend({ should be saved @param {Object} relationship metadata about the relationship being serialized */ + addHasMany: function(hash, record, key, relationship) { var type = record.constructor, name = relationship.key, serializedHasMany = [], + includeType = (relationship.options && relationship.options.polymorphic), manyArray, embeddedType; // If the has-many is not embedded, there is nothing to do. @@ -6519,7 +7068,7 @@ DS.JSONSerializer = DS.Serializer.extend({ // Build up the array of serialized records manyArray.forEach(function (record) { - serializedHasMany.push(this.serialize(record, { includeId: true })); + serializedHasMany.push(this.serialize(record, { includeId: true, includeType: includeType })); }, this); // Set the appropriate property of the serialized JSON to the @@ -6527,6 +7076,11 @@ DS.JSONSerializer = DS.Serializer.extend({ hash[key] = serializedHasMany; }, + addType: function(hash, type) { + var keyForType = this.keyForEmbeddedType(); + hash[keyForType] = this.rootForType(type); + }, + // EXTRACTION extract: function(loader, json, type, record) { @@ -6538,6 +7092,8 @@ DS.JSONSerializer = DS.Serializer.extend({ if (json[root]) { if (record) { loader.updateId(record, json[root]); } this.extractRecordRepresentation(loader, type, json[root]); + } else { + Ember.Logger.warn("Extract requested, but no data given for " + type + ". This may cause weird problems."); } }, @@ -6563,12 +7119,32 @@ DS.JSONSerializer = DS.Serializer.extend({ }, extractMeta: function(loader, type, json) { - var meta = json[this.configOption(type, 'meta')], since; - if (!meta) { return; } + var meta = this.configOption(type, 'meta'), + data = json, value; + + if(meta && json[meta]){ + data = json[meta]; + } + + this.metadataMapping.forEach(function(property, key){ + value = data[property]; + if(!Ember.isNone(value)){ + loader.metaForType(type, key, value); + } + }); + }, + + extractEmbeddedType: function(relationship, data) { + var foundType = relationship.type; + if(relationship.options && relationship.options.polymorphic) { + var key = this.keyFor(relationship), + keyForEmbeddedType = this.keyForEmbeddedType(key); - if (since = meta[this.configOption(type, 'since')]) { - loader.sinceForType(type, since); + foundType = this.typeFromAlias(data[keyForEmbeddedType]); + delete data[keyForEmbeddedType]; } + + return foundType; }, /** @@ -6590,20 +7166,17 @@ DS.JSONSerializer = DS.Serializer.extend({ 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')) { + !!this.metadataMapping.get(prop)) { 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); + sideloadedType = this.typeFromAlias(prop); + Ember.assert("Your server returned a hash with the key " + prop + " but you have no mapping for it", !!sideloadedType); this.loadValue(loader, sideloadedType, json[prop]); } @@ -6612,26 +7185,6 @@ DS.JSONSerializer = DS.Serializer.extend({ /** @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. @@ -6645,11 +7198,9 @@ DS.JSONSerializer = DS.Serializer.extend({ type.eachRelatedType(function(relatedType) { if (!configured.contains(relatedType)) { - var root = this.sideloadMappingForType(relatedType); - if (!root) { - root = this.defaultSideloadRootForType(relatedType); - this.sideloadMapping.set(root, relatedType); - } + var root = this.defaultSideloadRootForType(relatedType); + this.aliases.set(root, relatedType); + this.configureSideloadMappingForType(relatedType, configured); } }, this); @@ -6665,33 +7216,44 @@ DS.JSONSerializer = DS.Serializer.extend({ } }, - // HELPERS + /** + A hook you can use in your serializer subclass to customize + how a polymorphic association's name is converted into a key for the id. - // 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"; + @param {String} name the association name to convert into a key + + @return {String} the key + */ + keyForPolymorphicId: function(key){ + return key; }, - // 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; - } + /** + A hook you can use in your serializer subclass to customize + how a polymorphic association's name is converted into a key for the type. + + @param {String} name the association name to convert into a key + + @return {String} the key + */ + keyForPolymorphicType: function(key){ + return this.keyForPolymorphicId(key) + '_type'; + }, + + /** + A hook you can use in your serializer subclass to customize + the key used to store the type of a record of an embedded polymorphic association. + + By default, this method return 'type'. + + @return {String} the key + */ + keyForEmbeddedType: function() { + return 'type'; }, + // HELPERS + /** @private @@ -6702,7 +7264,7 @@ DS.JSONSerializer = DS.Serializer.extend({ `user_group`. @param {DS.Model subclass} type - @returns {String} name of the root element + @return {String} name of the root element */ rootForType: function(type) { var typeString = type.toString(); @@ -6718,26 +7280,10 @@ DS.JSONSerializer = DS.Serializer.extend({ /** @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 + @return {String} name of the root element */ defaultSideloadRootForType: function(type) { return this.pluralize(this.rootForType(type)); @@ -6749,6 +7295,13 @@ DS.JSONSerializer = DS.Serializer.extend({ (function() { +/** + @module data + @submodule data-adapter +*/ + +var get = Ember.get, set = Ember.set, merge = Ember.merge; + function loaderFor(store) { return { load: function(type, data, prematerialized) { @@ -6766,7 +7319,7 @@ function loaderFor(store) { populateArray: Ember.K, sideload: function(type, data) { - return store.load(type, data); + return store.adapterForType(type).load(store, type, data); }, sideloadMany: function(type, array) { @@ -6774,11 +7327,11 @@ function loaderFor(store) { }, prematerialize: function(reference, prematerialized) { - store.prematerialize(reference, prematerialized); + reference.prematerialized = prematerialized; }, - sinceForType: function(type, since) { - store.sinceForType(type, since); + metaForType: function(type, property, data) { + store.metaForType(type, property, data); } }; } @@ -6802,7 +7355,6 @@ DS.loaderFor = loaderFor; To tell your store which adapter to use, set its `adapter` property: App.store = DS.Store.create({ - revision: 3, adapter: App.MyAdapter.create() }); @@ -6815,17 +7367,22 @@ DS.loaderFor = loaderFor; * `updateRecord()` * `deleteRecord()` - To improve the network performance of your application, you can optimize - your adapter by overriding these lower-level methods: + 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; + For an example implementation, see {{#crossLink "DS.RestAdapter"}} the + included REST adapter.{{/crossLink}}. + + @class Adapter + @namespace DS + @extends Ember.Object +*/ DS.Adapter = Ember.Object.extend(DS._Mappable, { @@ -6862,28 +7419,27 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { For example: - ```javascript - adapter.load(store, App.Person, { - id: 123, - firstName: "Yehuda", - lastName: "Katz", - occupations: [{ - id: 345, - title: "Tricycle Mechanic" - }] - }); - ``` + adapter.load(store, App.Person, { + id: 123, + firstName: "Yehuda", + lastName: "Katz", + occupations: [{ + id: 345, + title: "Tricycle Mechanic" + }] + }); This will load the payload for the `App.Person` with ID `123` and the embedded `App.Occupation` with ID `345`. + @method load @param {DS.Store} store @param {subclass of DS.Model} type @param {any} payload */ load: function(store, type, payload) { var loader = loaderFor(store); - get(this, 'serializer').extractRecordRepresentation(loader, type, payload); + return get(this, 'serializer').extractRecordRepresentation(loader, type, payload); }, /** @@ -6900,12 +7456,13 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { 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. + it is passed as the payload. + @method didCreateRecord @param {DS.Store} store @param {subclass of DS.Model} type @param {DS.Model} record - @param {any} payload + @param {any} payload */ didCreateRecord: function(store, type, record, payload) { store.didSaveRecord(record); @@ -6926,17 +7483,18 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { 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 + has saved multiple created records to its persistent storage received an acknowledgement. If the persistent storage returns a new payload in response to the creation, and you want to update the existing record with the new information, pass the payload as the fourth parameter. + @method didCreateRecords @param {DS.Store} store @param {subclass of DS.Model} type @param {DS.Model} record - @param {any} payload + @param {any} payload */ didCreateRecords: function(store, type, records, payload) { records.forEach(function(record) { @@ -6962,16 +7520,16 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { update or delete, and you want to update the existing record with the new information, pass the payload as the fourth parameter. + @method didSaveRecord @param {DS.Store} store @param {subclass of DS.Model} type @param {DS.Model} record - @param {any} payload + @param {any} payload */ didSaveRecord: function(store, type, record, payload) { store.didSaveRecord(record); - var serializer = get(this, 'serializer'), - mappings = serializer.mappingForType(type); + var serializer = get(this, 'serializer'); serializer.eachEmbeddedRecord(record, function(embeddedRecord, embeddedType) { if (embeddedType === 'load') { return; } @@ -6995,10 +7553,11 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { If the persistent storage returns a new payload in response to the update, pass the payload as the fourth parameter. + @method didUpdateRecord @param {DS.Store} store @param {subclass of DS.Model} type @param {DS.Model} record - @param {any} payload + @param {any} payload */ didUpdateRecord: function() { this.didSaveRecord.apply(this, arguments); @@ -7014,17 +7573,18 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { If the persistent storage returns a new payload in response to the deletion, pass the payload as the fourth parameter. + @method didDeleteRecord @param {DS.Store} store @param {subclass of DS.Model} type @param {DS.Model} record - @param {any} payload + @param {any} payload */ didDeleteRecord: function() { this.didSaveRecord.apply(this, arguments); }, /** - Acknowledges that the adapter has finished updating or deleting + Acknowledges that the adapter has finished updating or deleting multiple records. Your adapter should call this method from its `updateRecords` or @@ -7034,10 +7594,11 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { If the persistent storage returns a new payload in response to the creation, pass the payload as the fourth parameter. + @method didSaveRecords @param {DS.Store} store @param {subclass of DS.Model} type @param {DS.Model} records - @param {any} payload + @param {any} payload */ didSaveRecords: function(store, type, records, payload) { records.forEach(function(record) { @@ -7060,10 +7621,11 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { If the persistent storage returns a new payload in response to the update, pass the payload as the fourth parameter. + @method didUpdateRecords @param {DS.Store} store @param {subclass of DS.Model} type @param {DS.Model} records - @param {any} payload + @param {any} payload */ didUpdateRecords: function() { this.didSaveRecords.apply(this, arguments); @@ -7079,10 +7641,11 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { If the persistent storage returns a new payload in response to the deletion, pass the payload as the fourth parameter. + @method didDeleteRecords @param {DS.Store} store @param {subclass of DS.Model} type @param {DS.Model} records - @param {any} payload + @param {any} payload */ didDeleteRecords: function() { this.didSaveRecords.apply(this, arguments); @@ -7098,9 +7661,10 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { to your find method so that the store knows which record to associate the new data with. + @method didFindRecord @param {DS.Store} store @param {subclass of DS.Model} type - @param {any} payload + @param {any} payload @param {String} id */ didFindRecord: function(store, type, payload, id) { @@ -7122,9 +7686,10 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { You adapter should call this method from its `findAll` method with the response from the backend. + @method didFindAll @param {DS.Store} store @param {subclass of DS.Model} type - @param {any} payload + @param {any} payload */ didFindAll: function(store, type, payload) { var loader = DS.loaderFor(store), @@ -7141,9 +7706,10 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { Your adapter should call this method from its `findQuery` method with the response from the backend. + @method didFindQuery @param {DS.Store} store @param {subclass of DS.Model} type - @param {any} payload + @param {any} payload @param {DS.AdapterPopulatedRecordArray} recordArray */ didFindQuery: function(store, type, payload, recordArray) { @@ -7162,9 +7728,10 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { You adapter should call this method from its `findMany` method with the response from the backend. + @method didFindMany @param {DS.Store} store @param {subclass of DS.Model} type - @param {any} payload + @param {any} payload */ didFindMany: function(store, type, payload) { var loader = DS.loaderFor(store); @@ -7179,6 +7746,7 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { Your adapter should call this method to indicate that the backend returned an error for a request. + @method didError @param {DS.Store} store @param {subclass of DS.Model} type @param {DS.Model} record @@ -7217,6 +7785,7 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { Once it registers a transform for a given type, it ignores subsequent transforms for the same attribute type. + @method registerSerializerTransforms @param {Class} klass the DS.Adapter subclass to extract the transforms from @param {DS.Serializer} serializer the serializer to register @@ -7225,6 +7794,7 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { */ registerSerializerTransforms: function(klass, serializer, seen) { var transforms = klass._registeredTransforms, superclass, prop; + var enumTransforms = klass._registeredEnumTransforms; for (prop in transforms) { if (!transforms.hasOwnProperty(prop) || prop in seen) { continue; } @@ -7233,6 +7803,13 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { serializer.registerTransform(prop, transforms[prop]); } + for (prop in enumTransforms) { + if (!enumTransforms.hasOwnProperty(prop) || prop in seen) { continue; } + seen[prop] = true; + + serializer.registerEnumTransform(prop, enumTransforms[prop]); + } + if (superclass = klass.superclass) { this.registerSerializerTransforms(superclass, serializer, seen); } @@ -7245,6 +7822,7 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { registers any class-registered mappings on the adapter's serializer. + @method registerSerializerMappings @param {Class} klass the DS.Adapter subclass to extract the transforms from @param {DS.Serializer} serializer the serializer to register the @@ -7267,17 +7845,19 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { Here is an example `find` implementation: - find: function(store, type, id) { - var url = type.url; - url = url.fmt(id); + 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); - }); - } + jQuery.getJSON(url, function(data) { + // data is a hash of key/value pairs. If your server returns a + // root, simply do something like: + // store.load(type, id, data.person) + store.load(type, id, data); + }); + } + + @method find */ find: null, @@ -7288,7 +7868,7 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { }, /** - A public method that allows you to register an enumerated + 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. @@ -7296,21 +7876,23 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { 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']); + 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') - }); + + 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} } + App: myTask.get('priority') => 'low' + Server Response / Load: { myTask: {priority: 0} } + @method registerEnumTransform @param {String} type of the transform - @param {Array} array of String objects to use for the enumerated values. + @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) { @@ -7335,6 +7917,10 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { var uuid = App.generateUUIDWithStatisticallyLowOddsOfCollision(); return uuid; } + + @method generateIdForRecord + @param {DS.Store} store + @param {DS.Model} record */ generateIdForRecord: null, @@ -7430,6 +8016,14 @@ DS.Adapter.reopenClass({ this._registeredTransforms = registeredTransforms; }, + registerEnumTransform: function(attributeType, objects) { + var registeredEnumTransforms = this._registeredEnumTransforms || {}; + + registeredEnumTransforms[attributeType] = objects; + + this._registeredEnumTransforms = registeredEnumTransforms; + }, + map: DS._Mappable.generateMapFunctionFor('attributes', function(key, newValue, map) { var existingValue = map.get(key); @@ -7450,7 +8044,7 @@ DS.Adapter.reopenClass({ merge(existingValue, newValue); }), - resolveMapConflict: function(oldValue, newValue, mappingsKey) { + resolveMapConflict: function(oldValue, newValue) { merge(newValue, oldValue); return newValue; @@ -7462,6 +8056,18 @@ DS.Adapter.reopenClass({ (function() { +/** + @module data + @submodule data-serializers +*/ + +/** + @class FixtureSerializer + @constructor + @namespace DS + @extends DS.Serializer +*/ + var get = Ember.get, set = Ember.set; DS.FixtureSerializer = DS.Serializer.extend({ @@ -7494,18 +8100,6 @@ DS.FixtureSerializer = DS.Serializer.extend({ 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); @@ -7548,7 +8142,32 @@ DS.FixtureSerializer = DS.Serializer.extend({ }, extractBelongsTo: function(type, hash, key) { - return hash[key]; + var val = hash[key]; + if (val != null) { + val = val + ''; + } + return val; + }, + + extractBelongsToPolymorphic: function(type, hash, key) { + var keyForId = this.keyForPolymorphicId(key), + keyForType, + id = hash[keyForId]; + + if (id) { + keyForType = this.keyForPolymorphicType(key); + return {id: id, type: hash[keyForType]}; + } + + return null; + }, + + keyForPolymorphicId: function(key) { + return key; + }, + + keyForPolymorphicType: function(key) { + return key + '_type'; } }); @@ -7557,6 +8176,11 @@ DS.FixtureSerializer = DS.Serializer.extend({ (function() { +/** + @module data + @submodule data-adapters +*/ + var get = Ember.get, fmt = Ember.String.fmt, dump = Ember.get(window, 'JSON.stringify') || function(object) { return object.toString(); }; @@ -7569,6 +8193,10 @@ var get = Ember.get, fmt = Ember.String.fmt, system would do. Its possible to do develop your entire application with `DS.FixtureAdapter`. + @class FixtureAdapter + @constructor + @namespace DS + @extends DS.Adapter */ DS.FixtureAdapter = DS.Adapter.extend({ @@ -7585,8 +8213,9 @@ DS.FixtureAdapter = DS.Adapter.extend({ 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)])); + var fixtureIdType = typeof fixture.id; + if(fixtureIdType !== "number" && fixtureIdType !== "string"){ + throw new Error(fmt('the id property must be defined as a number or string for fixture %@', [dump(fixture)])); } fixture.id = fixture.id + ''; return fixture; @@ -7721,8 +8350,6 @@ DS.FixtureAdapter = DS.Adapter.extend({ @private */ deleteLoadedFixture: function(type, record) { - var id = this.extractId(type, record); - var existingFixture = this.findExistingFixture(type, record); if(existingFixture) { @@ -7740,8 +8367,6 @@ DS.FixtureAdapter = DS.Adapter.extend({ }, findFixtureById: function(fixtures, id) { - var adapter = this; - return Ember.A(fixtures).find(function(r) { if(''+get(r, 'id') === ''+id) { return true; @@ -7767,6 +8392,20 @@ DS.FixtureAdapter = DS.Adapter.extend({ (function() { +/** + @module data + @submodule data-serializers +*/ + +/** + @class RESTSerializer + @constructor + @namespace DS + @extends DS.Serializer +*/ + +var get = Ember.get; + DS.RESTSerializer = DS.JSONSerializer.extend({ keyForAttributeName: function(type, name) { return Ember.String.decamelize(name); @@ -7790,6 +8429,27 @@ DS.RESTSerializer = DS.JSONSerializer.extend({ } return this.singularize(key) + "_ids"; + }, + + keyForPolymorphicId: function(key) { + return key; + }, + + keyForPolymorphicType: function(key) { + return key.replace(/_id$/, '_type'); + }, + + extractValidationErrors: function(type, json) { + var errors = {}; + + get(type, 'attributes').forEach(function(name) { + var key = this._keyForAttributeName(type, name); + if (json['errors'].hasOwnProperty(key)) { + errors[name] = json['errors'][key]; + } + }, this); + + return errors; } }); @@ -7798,9 +8458,18 @@ DS.RESTSerializer = DS.JSONSerializer.extend({ (function() { -/*global jQuery*/ +/** + @module data + @submodule data-adapters +*/ -var get = Ember.get, set = Ember.set, merge = Ember.merge; +var get = Ember.get, set = Ember.set; + +DS.rejectionHandler = function(reason) { + Ember.Logger.assert([reason, reason.message, reason.stack]); + + throw reason; +}; /** The REST adapter allows your store to communicate with an HTTP server by @@ -7856,8 +8525,14 @@ var get = Ember.get, set = Ember.set, merge = Ember.merge; } } ``` + + @class RESTAdapter + @constructor + @namespace DS + @extends DS.Adapter */ DS.RESTAdapter = DS.Adapter.extend({ + namespace: null, bulkCommit: false, since: 'since', @@ -7873,26 +8548,6 @@ DS.RESTAdapter = DS.Adapter.extend({ 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); }, @@ -7924,7 +8579,26 @@ DS.RESTAdapter = DS.Adapter.extend({ } }, + createRecord: function(store, type, record) { + var root = this.rootForType(type); + var adapter = this; + var data = {}; + + data[root] = this.serialize(record, { includeId: true }); + + return this.ajax(this.buildURL(root), "POST", { + data: data + }).then(function(json){ + adapter.didCreateRecord(store, type, record, json); + }, function(xhr) { + adapter.didError(store, type, record, xhr); + throw xhr; + }).then(null, DS.rejectionHandler); + }, + createRecords: function(store, type, records) { + var adapter = this; + if (get(this, 'bulkCommit') === false) { return this._super(store, type, records); } @@ -7938,153 +8612,144 @@ DS.RESTAdapter = DS.Adapter.extend({ 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); - }); - } - }); + return this.ajax(this.buildURL(root), "POST", { + data: data + }).then(function(json) { + adapter.didCreateRecords(store, type, records, json); + }).then(null, DS.rejectionHandler); }, updateRecord: function(store, type, record) { - var id = get(record, 'id'); - var root = this.rootForType(type); + var id, root, adapter, data; - var data = {}; + id = get(record, 'id'); + root = this.rootForType(type); + adapter = this; + + 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); - } - }); + return this.ajax(this.buildURL(root, id), "PUT",{ + data: data + }).then(function(json){ + adapter.didUpdateRecord(store, type, record, json); + }, function(xhr) { + adapter.didError(store, type, record, xhr); + throw xhr; + }).then(null, DS.rejectionHandler); }, updateRecords: function(store, type, records) { + var root, plural, adapter, data; + if (get(this, 'bulkCommit') === false) { return this._super(store, type, records); } - var root = this.rootForType(type), - plural = this.pluralize(root); + root = this.rootForType(type); + plural = this.pluralize(root); + adapter = this; + + data = {}; - 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); - }); - } - }); + return this.ajax(this.buildURL(root, "bulk"), "PUT", { + data: data + }).then(function(json) { + adapter.didUpdateRecords(store, type, records, json); + }).then(null, DS.rejectionHandler); }, deleteRecord: function(store, type, record) { - var id = get(record, 'id'); - var root = this.rootForType(type); + var id, root, adapter; - this.ajax(this.buildURL(root, id), "DELETE", { - context: this, - success: function(json) { - Ember.run(this, function(){ - this.didSaveRecord(store, type, record, json); - }); - } - }); + id = get(record, 'id'); + root = this.rootForType(type); + adapter = this; + + return this.ajax(this.buildURL(root, id), "DELETE").then(function(json){ + adapter.didDeleteRecord(store, type, record, json); + }, function(xhr){ + adapter.didError(store, type, record, xhr); + throw xhr; + }).then(null, DS.rejectionHandler); }, deleteRecords: function(store, type, records) { + var root, plural, serializer, adapter, data; + if (get(this, 'bulkCommit') === false) { return this._super(store, type, records); } - var root = this.rootForType(type), - plural = this.pluralize(root), - serializer = get(this, 'serializer'); + root = this.rootForType(type); + plural = this.pluralize(root); + serializer = get(this, 'serializer'); + adapter = this; + + data = {}; - 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); - }); - } - }); + return this.ajax(this.buildURL(root, 'bulk'), "DELETE", { + data: data + }).then(function(json){ + adapter.didDeleteRecords(store, type, records, json); + }).then(null, DS.rejectionHandler); }, find: function(store, type, id) { - var root = this.rootForType(type); + var root = this.rootForType(type), adapter = this; - this.ajax(this.buildURL(root, id), "GET", { - success: function(json) { - Ember.run(this, function(){ - this.didFindRecord(store, type, json, id); - }); - } - }); + return this.ajax(this.buildURL(root, id), "GET"). + then(function(json){ + adapter.didFindRecord(store, type, json, id); + }).then(null, DS.rejectionHandler); }, findAll: function(store, type, since) { - var root = this.rootForType(type); + var root, adapter; - this.ajax(this.buildURL(root), "GET", { - data: this.sinceQuery(since), - success: function(json) { - Ember.run(this, function(){ - this.didFindAll(store, type, json); - }); - } - }); + root = this.rootForType(type); + adapter = this; + + return this.ajax(this.buildURL(root), "GET",{ + data: this.sinceQuery(since) + }).then(function(json) { + adapter.didFindAll(store, type, json); + }).then(null, DS.rejectionHandler); }, findQuery: function(store, type, query, recordArray) { - var root = this.rootForType(type); + var root = this.rootForType(type), + adapter = this; - this.ajax(this.buildURL(root), "GET", { - data: query, - success: function(json) { - Ember.run(this, function(){ - this.didFindQuery(store, type, json, recordArray); - }); - } - }); + return this.ajax(this.buildURL(root), "GET", { + data: query + }).then(function(json){ + adapter.didFindQuery(store, type, json, recordArray); + }).then(null, DS.rejectionHandler); }, findMany: function(store, type, ids, owner) { - var root = this.rootForType(type); + var root = this.rootForType(type), + adapter = this; + 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); - }); - } - }); + return this.ajax(this.buildURL(root), "GET", { + data: {ids: ids} + }).then(function(json) { + adapter.didFindMany(store, type, json); + }).then(null, DS.rejectionHandler); }, /** @@ -8092,7 +8757,7 @@ DS.RESTAdapter = DS.Adapter.extend({ This method serializes a list of IDs using `serializeId` - @returns {Array} an array of serialized IDs + @return {Array} an array of serialized IDs */ serializeIds: function(ids) { var serializer = get(this, 'serializer'); @@ -8104,25 +8769,41 @@ DS.RESTAdapter = DS.Adapter.extend({ didError: function(store, type, record, xhr) { if (xhr.status === 422) { - var data = JSON.parse(xhr.responseText); - store.recordWasInvalid(record, data['errors']); + var json = JSON.parse(xhr.responseText), + serializer = get(this, 'serializer'), + errors = serializer.extractValidationErrors(type, json); + + store.recordWasInvalid(record, errors); } else { this._super.apply(this, arguments); } }, ajax: function(url, type, hash) { - hash.url = url; - hash.type = type; - hash.dataType = 'json'; - hash.contentType = 'application/json; charset=utf-8'; - hash.context = this; + var adapter = this; - if (hash.data && type !== 'GET') { - hash.data = JSON.stringify(hash.data); - } + return new Ember.RSVP.Promise(function(resolve, reject) { + hash = hash || {}; + hash.url = url; + hash.type = type; + hash.dataType = 'json'; + hash.context = adapter; + + if (hash.data && type !== 'GET') { + hash.contentType = 'application/json; charset=utf-8'; + hash.data = JSON.stringify(hash.data); + } + + hash.success = function(json) { + Ember.run(null, resolve, json); + }; - jQuery.ajax(hash); + hash.error = function(jqXHR, textStatus, errorThrown) { + Ember.run(null, reject, jqXHR); + }; + + Ember.$.ajax(hash); + }); }, url: "", @@ -8144,7 +8825,7 @@ DS.RESTAdapter = DS.Adapter.extend({ 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) { + if (!Ember.isNone(this.namespace)) { url.push(this.namespace); } @@ -8163,257 +8844,19 @@ DS.RESTAdapter = DS.Adapter.extend({ } }); - })(); (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 SaveProcessor(record, store, type, includeId) { - this.record = record; - ObjectProcessor.call(this, record.toJSON({ includeId: includeId }), type, store); -} - -SaveProcessor.prototype = Ember.create(ObjectProcessor.prototype); - -SaveProcessor.prototype.save = function(callback) { - callback(this.json); -}; - -function saveProcessorFactory(store, type, includeId) { - return function(record) { - return new SaveProcessor(record, store, type, includeId); - }; -} - -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, saveProcessorFactory(store, type)); - }, - - updateRecord: function(store, type, record) { - var sync = type.sync; - sync.updateRecord(record, saveProcessorFactory(store, type, true)); - }, - - deleteRecord: function(store, type, record) { - var sync = type.sync; - sync.deleteRecord(record, saveProcessorFactory(store, type, true)); - } -}); - -DS.registerTransforms = function(kind, object) { - registeredTransforms[kind] = object; -}; - -DS.clearTransforms = function() { - registeredTransforms = {}; -}; - -DS.clearTransforms(); - -})(); - +/** + Adapters included with Ember-Data. + @module data + @submodule data-adapters -(function() { + @main data-adapters +*/ })(); @@ -8440,5 +8883,12 @@ DS.clearTransforms(); //OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE //SOFTWARE. +/** + Ember Data + @module data +*/ + })(); + +})(); |