From 3b27a5d45b12f6bac65da2a8e17387bfda42a2f1 Mon Sep 17 00:00:00 2001 From: Stefan Wintermeyer Date: Thu, 20 Jun 2013 19:02:50 +0200 Subject: Update Ember, Ember-Data and Handlebars. --- public/js/libs/ember-data.js | 5450 +++++++++++++++++++++++------------------- 1 file changed, 2950 insertions(+), 2500 deletions(-) (limited to 'public/js/libs/ember-data.js') 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 + * © 2011 Colin Snover + * 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; } - // Make `add` idempotent - if (recordTransaction === this) { return; } + this.add(embeddedRecord); + }, this); - // XXX it should be possible to move a dirty transaction from the default transaction + this.adoptRecord(record); + }, - // 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')); + relationships: Ember.computed(function() { + var relationships = Ember.OrderedSet.create(), + records = get(this, 'records'), + store = get(this, 'store'); - Ember.assert("Models cannot belong to more than one transaction at a time.", recordTransaction === defaultTransaction); + 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]); + } + }); - this.adoptRecord(record); - }, + return relationships; + }).volatile(), - relationshipBecameDirty: function(relationship) { - get(this, 'relationships').add(relationship); - }, + commitDetails: Ember.computed(function() { + var commitDetails = Ember.MapWithDefault.create({ + defaultValue: function() { + return { + created: Ember.OrderedSet.create(), + updated: Ember.OrderedSet.create(), + deleted: Ember.OrderedSet.create() + }; + } + }); - relationshipBecameClean: function(relationship) { - get(this, 'relationships').remove(relationship); - }, + var records = get(this, 'records'), + store = get(this, 'store'); + + records.forEach(function(record) { + if(!get(record, 'isDirty')) return; + record.send('willCommit'); + var adapter = store.adapterForType(record.constructor); + commitDetails.get(adapter)[get(record, 'dirtyType')].add(record); + }); + + return commitDetails; + }).volatile(), /** Commits the transaction, which causes all of the modified records that @@ -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. - - @param {String} bucketType one of `clean`, `created`, `updated`, or `deleted` - */ - addToBucket: function(bucketType, record) { - this.bucketForType(bucketType).add(record); - }, - - /** - @private - - Removes a record from the named bucket. - - @param {String} bucketType one of `clean`, `created`, `updated`, or `deleted` - */ - removeFromBucket: function(bucketType, record) { - this.bucketForType(bucketType).remove(record); - }, - - /** - @private - - Called by a record's state manager to indicate that the record has entered - a dirty state. The record will be moved from the `clean` bucket and into - the appropriate dirty bucket. + @private - @param {String} bucketType one of `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. */ - 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,15 +1075,38 @@ 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]); + } -(function() { + return meta[keyName]; +} + +})(); + + + +(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(); + + Most Ember.js applications will only have a single `DS.Store` that is + automatically created by their `Ember.Application`. - MyApp.store = DS.Store.create(); + You can retrieve models from the store in several ways. To retrieve a record + for a specific id, use `DS.Model`'s `find()` method: - You can retrieve DS.Model instances from the store in several ways. To retrieve - a record for a specific id, use the `find()` method: + var person = App.Person.find(123); - var record = MyApp.store.find(MyApp.Contact, 123); + If your application has multiple `DS.Store` instances (an unusual case), you can + specify which store should be used: - 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: + 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,47 +1248,22 @@ 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. @@ -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 (this.hasReferenceForId(type, id)) { + reference = this.referenceForId(type, id); + + if (reference.data !== UNLOADED) { + return this.recordForReference(reference); + } + } - if (clientId) { - return this.findByClientId(type, clientId); + if (!reference) { + reference = this.createReference(type, id); } - clientId = this.pushData(LOADING, id, type); + 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. + This method returns a record for a given record refeence. - Several parts of Ember Data call this method: - - * findById, if a clientId already exists for a given type and - id combination - * OneToManyChange, which is backed by clientIds, when getChild, - getOldParent or getNewParent are called - * RecordArray, which is backed by clientIds, when an object at - a particular index is looked up + 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 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,1184 +4122,1343 @@ DS.attr = function(type, options) { (function() { +/** + @module data + @submodule data-model +*/ })(); (function() { -var get = Ember.get, set = Ember.set, - none = Ember.isNone; - -DS.belongsTo = function(type, options) { - Ember.assert("The first argument DS.belongsTo must be a model type or string, like DS.belongsTo(App.Person)", !!type && (typeof type === 'string' || DS.Model.detect(type))); - - options = options || {}; +/** + 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 meta = { type: type, isRelationship: true, options: options, kind: 'belongsTo' }; +var AttributeChange = DS.AttributeChange = function(options) { + this.reference = options.reference; + this.store = options.store; + this.name = options.name; + this.oldValue = options.oldValue; +}; - return Ember.computed(function(key, value) { - if (arguments.length === 2) { - return value === undefined ? null : value; - } +AttributeChange.createChange = function(options) { + return new AttributeChange(options); +}; - var data = get(this, 'data').belongsTo, - store = get(this, 'store'), id; +AttributeChange.prototype = { + sync: function() { + this.store.recordAttributeDidChange(this.reference, this.name, this.value, this.oldValue); - if (typeof type === 'string') { - type = get(this, type, false) || get(Ember.lookup, type); - } + // TODO: Use this object in the commit process + this.destroy(); + }, - id = data[key]; + /** + 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; - if(!id) { - return null; - } else if (typeof id === 'object') { - return store.recordForReference(id); - } else { - return store.find(type, id); - } - }).property('data').meta(meta); + delete record._changesToSync[this.name]; + } }; -/** - These observers observe all `belongsTo` relationships on the record. See - `relationships/ext` to see how these observers get their dependencies. +})(); -*/ -DS.Model.reopen({ - /** @private */ - belongsToWillChange: Ember.beforeObserver(function(record, key) { - if (get(record, 'isLoaded')) { - var oldParent = get(record, key); - var childReference = get(record, '_reference'), - store = get(record, 'store'); - if (oldParent){ - var change = DS.RelationshipChange.createChange(childReference, get(oldParent, '_reference'), store, { key: key, kind:"belongsTo", changeType: "remove" }); - change.sync(); - this._changesToSync[key] = change; - } - } - }), +(function() { +var get = Ember.get, set = Ember.set; +var forEach = Ember.EnumerableUtils.forEach; - /** @private */ - belongsToDidChange: Ember.immediateObserver(function(record, key) { - if (get(record, 'isLoaded')) { - var newParent = get(record, key); - if(newParent){ - var childReference = get(record, '_reference'), - store = get(record, 'store'); - var change = DS.RelationshipChange.createChange(childReference, get(newParent, '_reference'), store, { key: key, kind:"belongsTo", changeType: "add" }); - change.sync(); - if(this._changesToSync[key]){ - DS.OneToManyChange.ensureSameTransaction([change, this._changesToSync[key]], store); - } - } - } - delete this._changesToSync[key]; - }) -}); +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); +}; -(function() { -var get = Ember.get, set = Ember.set; -var hasRelationship = function(type, options) { - options = options || {}; +/** @private */ +DS.RelationshipChange.create = function(options) { + return new DS.RelationshipChange(options); +}; - var meta = { type: type, isRelationship: true, options: options, kind: 'hasMany' }; +/** @private */ +DS.RelationshipChangeAdd.create = function(options) { + return new DS.RelationshipChangeAdd(options); +}; - return Ember.computed(function(key, value) { - var data = get(this, 'data').hasMany, - store = get(this, 'store'), - ids, relationship; +/** @private */ +DS.RelationshipChangeRemove.create = function(options) { + return new DS.RelationshipChangeRemove(options); +}; - if (typeof type === 'string') { - type = get(this, type, false) || get(Ember.lookup, type); +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"; } + } + +}; - ids = data[key]; - relationship = store.findMany(type, ids, this, meta); - set(relationship, 'owner', this); - set(relationship, 'name', key); +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); + } +}; - return relationship; - }).property().meta(meta); +/** @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; }; -DS.hasMany = function(type, options) { - Ember.assert("The type passed to DS.hasMany must be defined", !!type); - return hasRelationship(type, options); +/** @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 + }); -(function() { -var get = Ember.get, set = Ember.set; + store.addRelationshipChangeFor(childReference, key, parentReference, null, change); -/** - @private - This file defines several extensions to the base `DS.Model` class that - add support for one-to-many relationships. -*/ + return change; +}; -DS.Model.reopen({ - // This Ember.js hook allows an object to be notified when a property - // is defined. - // - // In this case, we use it to be notified when an Ember Data user defines a - // belongs-to relationship. In that case, we need to set up observers for - // each one, allowing us to track relationship changes and automatically - // reflect changes in the inverse has-many array. - // - // This hook passes the class being set up, as well as the key and value - // being defined. So, for example, when the user does this: - // - // DS.Model.extend({ - // parent: DS.belongsTo(App.User) - // }); - // - // This hook would be called with "parent" as the key and the computed - // property returned by `DS.belongsTo` as the value. - didDefineProperty: function(proto, key, value) { - // Check if the value being set is a computed property. - if (value instanceof Ember.Descriptor) { +/** @private */ +DS.OneToOneChange.createChange = function(childReference, parentReference, store, options) { + var key; - // If it is, get the metadata for the relationship. This is - // populated by the `DS.belongsTo` helper when it is creating - // the computed property. - var meta = value.meta(); + // If 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); + } - if (meta.isRelationship && meta.kind === 'belongsTo') { - Ember.addObserver(proto, key, null, 'belongsToDidChange'); - Ember.addBeforeObserver(proto, key, null, 'belongsToWillChange'); - } + var change = DS.RelationshipChange._createChange({ + parentReference: parentReference, + childReference: childReference, + firstRecordReference: childReference, + secondRecordReference: parentReference, + firstRecordKind: "belongsTo", + secondRecordKind: "belongsTo", + store: store, + changeType: options.changeType, + firstRecordName: key + }); - if (meta.isAttribute) { - Ember.addObserver(proto, key, null, 'attributeDidChange'); - Ember.addBeforeObserver(proto, key, null, 'attributeWillChange'); - } + store.addRelationshipChangeFor(childReference, key, parentReference, null, change); - meta.parentType = proto.constructor; + + 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(); } } -}); +}; -/** - These DS.Model extensions add class methods that provide relationship - introspection abilities about relationships. +/** @private */ +DS.OneToManyChange.createChange = function(childReference, parentReference, store, options) { + var key; - A note about the computed properties contained here: + // 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); + } - **These properties are effectively sealed once called for the first time.** - To avoid repeatedly doing expensive iteration over a model's fields, these - values are computed once and then cached for the remainder of the runtime of - your application. + var change = DS.RelationshipChange._createChange({ + parentReference: parentReference, + childReference: childReference, + firstRecordReference: childReference, + secondRecordReference: parentReference, + firstRecordKind: "belongsTo", + secondRecordKind: "hasMany", + store: store, + changeType: options.changeType, + firstRecordName: key + }); - If your application needs to modify a class after its initial definition - (for example, using `reopen()` to add additional attributes), make sure you - do it before using your model with the store, which uses these properties - extensively. -*/ + 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; + }, -DS.Model.reopenClass({ - /** - For a given relationship name, returns the model type of the relationship. + callChangeEvents: function(){ + var child = this.getFirstRecord(), + parentRecord = this.getSecondRecord(); - For example, if you define a model like this: + var dirtySet = new Ember.OrderedSet(); - App.Post = DS.Model.extend({ - comments: DS.hasMany(App.Comment) - }); + // 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); + } - Calling `App.Post.typeForRelationship('comments')` will return `App.Comment`. + if (child) { + this.store.recordBelongsToDidChange(dirtySet, child, this); + } - @param {String} name the name of the relationship - @return {subclass of DS.Model} the type of the relationship, or undefined - */ - typeForRelationship: function(name) { - var relationship = get(this, 'relationshipsByName').get(name); - return relationship && relationship.type; + dirtySet.forEach(function(record) { + record.adapterDidDirty(); + }); }, - /** - The model's relationships as a map, keyed on the type of the - relationship. The value of each entry is an array containing a descriptor - for each relationship with that type, describing the name of the relationship - as well as the type. + 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(); + } + }); + } +}; - For example, given the following model definition: +DS.RelationshipChangeAdd.prototype = Ember.create(DS.RelationshipChange.create({})); +DS.RelationshipChangeRemove.prototype = Ember.create(DS.RelationshipChange.create({})); - App.Blog = DS.Model.extend({ - users: DS.hasMany(App.User), - owner: DS.belongsTo(App.User), - posts: DS.hasMany(App.Post) - }); +DS.RelationshipChangeAdd.prototype.changeType = "add"; +DS.RelationshipChangeAdd.prototype.sync = function() { + var secondRecordName = this.getSecondRecordName(), + firstRecordName = this.getFirstRecordName(), + firstRecord = this.getFirstRecord(), + secondRecord = this.getSecondRecord(); - This computed property would return a map describing these - relationships, like this: + //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 relationships = Ember.get(App.Blog, 'relationships'); - associatons.get(App.User); - //=> [ { name: 'users', kind: 'hasMany' }, - // { name: 'owner', kind: 'belongsTo' } ] - relationships.get(App.Post); - //=> [ { name: 'posts', kind: 'hasMany' } ] + this.ensureSameTransaction(); - @type Ember.Map - @readOnly - */ - relationships: Ember.computed(function() { - var map = new Ember.MapWithDefault({ - defaultValue: function() { return []; } - }); + this.callChangeEvents(); - // Loop through each computed property on the class - this.eachComputedProperty(function(name, meta) { + if (secondRecord && firstRecord) { + if(this.secondRecordKind === "belongsTo"){ + secondRecord.suspendRelationshipObservers(function(){ + set(secondRecord, secondRecordName, firstRecord); + }); - // If the computed property is a relationship, add - // it to the map. - if (meta.isRelationship) { - if (typeof meta.type === 'string') { - meta.type = Ember.get(Ember.lookup, meta.type); - } + } + else if(this.secondRecordKind === "hasMany"){ + secondRecord.suspendRelationshipObservers(function(){ + get(secondRecord, secondRecordName).addObject(firstRecord); + }); + } + } - var relationshipsForType = map.get(meta.type); + 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); + }); + } + } - relationshipsForType.push({ name: name, kind: meta.kind }); - } - }); + this.coalesce(); +}; - return map; - }), +DS.RelationshipChangeRemove.prototype.changeType = "remove"; +DS.RelationshipChangeRemove.prototype.sync = function() { + var secondRecordName = this.getSecondRecordName(), + firstRecordName = this.getFirstRecordName(), + firstRecord = this.getFirstRecord(), + secondRecord = this.getSecondRecord(); - /** - A hash containing lists of the model's relationships, grouped - by the relationship kind. For example, given a model with this - definition: + //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); - App.Blog = DS.Model.extend({ - users: DS.hasMany(App.User), - owner: DS.belongsTo(App.User), + this.ensureSameTransaction(firstRecord, secondRecord, secondRecordName, firstRecordName); - posts: DS.hasMany(App.Post) - }); + this.callChangeEvents(); - This property would contain the following: + 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); + }); + } + } - var relationshipNames = Ember.get(App.Blog, 'relationshipNames'); - relationshipNames.hasMany; - //=> ['users', 'posts'] - relationshipNames.belongsTo; - //=> ['owner'] + 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); + }); + } + } - @type Object - @readOnly - */ - relationshipNames: Ember.computed(function() { - var names = { hasMany: [], belongsTo: [] }; + this.coalesce(); +}; - this.eachComputedProperty(function(name, meta) { - if (meta.isRelationship) { - names[meta.kind].push(name); - } - }); +})(); - return names; - }), - /** - An array of types directly related to a model. Each type will be - included once, regardless of the number of relationships it has with - the model. - For example, given a model with this definition: +(function() { +/** + @module data + @submodule data-changes +*/ - App.Blog = DS.Model.extend({ - users: DS.hasMany(App.User), - owner: DS.belongsTo(App.User), - posts: DS.hasMany(App.Post) - }); +})(); - This property would contain the following: - var relatedTypes = Ember.get(App.Blog, 'relatedTypes'); - //=> [ App.User, App.Post ] - @type Ember.Array - @readOnly - */ - relatedTypes: Ember.computed(function() { - var type, - types = Ember.A([]); +(function() { +var get = Ember.get, set = Ember.set, + isNone = Ember.isNone; - // Loop through each computed property on the class, - // and create an array of the unique types involved - // in relationships - this.eachComputedProperty(function(name, meta) { - if (meta.isRelationship) { - type = meta.type; +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))); - if (typeof type === 'string') { - type = get(this, type, false) || get(Ember.lookup, type); - } + options = options || {}; - if (!types.contains(type)) { - Ember.assert("Trying to sideload " + name + " on " + this.toString() + " but the type doesn't exist.", !!type); - types.push(type); - } - } - }); + var meta = { type: type, isRelationship: true, options: options, kind: 'belongsTo' }; - return types; - }), + return Ember.computed(function(key, value) { + if (typeof type === 'string') { + type = get(this, type, false) || get(Ember.lookup, type); + } - /** - A map whose keys are the relationships of a model and whose values are - relationship descriptors. + 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; + } - For example, given a model with this - definition: + var data = get(this, 'data').belongsTo, + store = get(this, 'store'), belongsTo; - App.Blog = DS.Model.extend({ - users: DS.hasMany(App.User), - owner: DS.belongsTo(App.User), + belongsTo = data[key]; - posts: DS.hasMany(App.Post) - }); + // TODO (tomdale) The value of the belongsTo in the data hash can be + // one of: + // 1. null/undefined + // 2. a record reference + // 3. a tuple returned by the serializer's polymorphism code + // + // We should really normalize #3 to be the same as #2 to reduce the + // complexity here. + + if (isNone(belongsTo)) { + return null; + } + + // The data has been normalized to a record reference, so + // just ask the store for the record for that reference, + // materializing it if necessary. + if (belongsTo.clientId) { + return store.recordForReference(belongsTo); + } + + // The data has been normalized into a type/id pair by the + // serializer's polymorphism code. + return store.findById(belongsTo.type, belongsTo.id); + }).property('data').meta(meta); +}; - This property would contain the following: +/** + These observers observe all `belongsTo` relationships on the record. See + `relationships/ext` to see how these observers get their dependencies. - var relationshipsByName = Ember.get(App.Blog, 'relationshipsByName'); - relationshipsByName.get('users'); - //=> { key: 'users', kind: 'hasMany', type: App.User } - relationshipsByName.get('owner'); - //=> { key: 'owner', kind: 'belongsTo', type: App.User } +*/ - @type Ember.Map - @readOnly - */ - relationshipsByName: Ember.computed(function() { - var map = Ember.Map.create(), type; +DS.Model.reopen({ + /** @private */ + belongsToWillChange: Ember.beforeObserver(function(record, key) { + if (get(record, 'isLoaded')) { + var oldParent = get(record, key); - this.eachComputedProperty(function(name, meta) { - if (meta.isRelationship) { - meta.key = name; - type = meta.type; + var childReference = get(record, '_reference'), + store = get(record, 'store'); + if (oldParent){ + var change = DS.RelationshipChange.createChange(childReference, get(oldParent, '_reference'), store, { key: key, kind:"belongsTo", changeType: "remove" }); + change.sync(); + this._changesToSync[key] = change; + } + } + }), - if (typeof type === 'string') { - type = get(this, type, false) || get(Ember.lookup, type); - meta.type = type; + /** @private */ + belongsToDidChange: Ember.immediateObserver(function(record, key) { + if (get(record, 'isLoaded')) { + var newParent = get(record, key); + if(newParent){ + var childReference = get(record, '_reference'), + store = get(record, 'store'); + var change = DS.RelationshipChange.createChange(childReference, get(newParent, '_reference'), store, { key: key, kind:"belongsTo", changeType: "add" }); + change.sync(); + if(this._changesToSync[key]){ + DS.OneToManyChange.ensureSameTransaction([change, this._changesToSync[key]], store); } - - map.set(name, meta); } - }); + } + delete this._changesToSync[key]; + }) +}); - return map; - }), +})(); - /** - A map whose keys are the fields of the model and whose values are strings - describing the kind of the field. A model's fields are the union of all of its - attributes and relationships. - For example: - App.Blog = DS.Model.extend({ - users: DS.hasMany(App.User), - owner: DS.belongsTo(App.User), +(function() { +var get = Ember.get, set = Ember.set, forEach = Ember.EnumerableUtils.forEach; +var hasRelationship = function(type, options) { + options = options || {}; - posts: DS.hasMany(App.Post), + var meta = { type: type, isRelationship: true, options: options, kind: 'hasMany' }; - title: DS.attr('string') - }); + return Ember.computed(function(key, value) { + var data = get(this, 'data').hasMany, + store = get(this, 'store'), + ids, relationship; - var fields = Ember.get(App.Blog, 'fields'); - fields.forEach(function(field, kind) { - console.log(field, kind); - }); + if (typeof type === 'string') { + type = get(this, type, false) || get(Ember.lookup, type); + } - // prints: - // users, hasMany - // owner, belongsTo - // posts, hasMany - // title, attribute + //ids can be references or opaque token + //(e.g. `{url: '/relationship'}`) that will be passed to the adapter + ids = data[key]; - @type Ember.Map - @readOnly - */ - fields: Ember.computed(function() { - var map = Ember.Map.create(), type; + relationship = store.findMany(type, ids, this, meta); + set(relationship, 'owner', this); + set(relationship, 'name', key); + set(relationship, 'isPolymorphic', options.polymorphic); - this.eachComputedProperty(function(name, meta) { - if (meta.isRelationship) { - map.set(name, meta.kind); - } else if (meta.isAttribute) { - map.set(name, 'attribute'); - } - }); + return relationship; + }).property().meta(meta); +}; - return map; - }), +DS.hasMany = function(type, options) { + Ember.assert("The type passed to DS.hasMany must be defined", !!type); + return hasRelationship(type, options); +}; - /** - Given a callback, iterates over each of the relationships in the model, - invoking the callback with the name of each relationship and its relationship - descriptor. +function clearUnmaterializedHasMany(record, relationship) { + var data = get(record, 'data').hasMany; - @param {Function} callback the callback to invoke - @param {any} binding the value to which the callback's `this` should be bound - */ - eachRelationship: function(callback, binding) { - get(this, 'relationshipsByName').forEach(function(name, relationship) { - callback.call(binding, name, relationship); - }); - }, + var references = data[relationship.key]; - /** - Given a callback, iterates over each of the types related to a model, - invoking the callback with the related type's class. Each type will be - returned just once, regardless of how many different relationships it has - with a model. + if (!references) { return; } - @param {Function} callback the callback to invoke - @param {any} binding the value to which the callback's `this` should be bound - */ - eachRelatedType: function(callback, binding) { - get(this, 'relatedTypes').forEach(function(type) { - callback.call(binding, type); + 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({ - /** - Given a callback, iterates over each of the relationships in the model, - invoking the callback with the name of each relationship and its relationship - descriptor. + clearHasMany: function(relationship) { + var hasMany = this.cacheFor(relationship.name); - @param {Function} callback the callback to invoke - @param {any} binding the value to which the callback's `this` should be bound - */ - eachRelationship: function(callback, binding) { - this.constructor.eachRelationship(callback, binding); + if (hasMany) { + hasMany.clear(); + } else { + clearUnmaterializedHasMany(this, relationship); + } } }); -/** - @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. +(function() { +var get = Ember.get, set = Ember.set; - 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. +/** + @private - For example, imagine the following two associated models: + This file defines several extensions to the base `DS.Model` class that + add support for one-to-many relationships. +*/ - App.Post = DS.Model.extend({ - comments: DS.hasMany('App.Comment') - }); +DS.Model.reopen({ + // This Ember.js hook allows an object to be notified when a property + // is defined. + // + // In this case, we use it to be notified when an Ember Data user defines a + // belongs-to relationship. In that case, we need to set up observers for + // each one, allowing us to track relationship changes and automatically + // reflect changes in the inverse has-many array. + // + // This hook passes the class being set up, as well as the key and value + // being defined. So, for example, when the user does this: + // + // DS.Model.extend({ + // parent: DS.belongsTo(App.User) + // }); + // + // This hook would be called with "parent" as the key and the computed + // property returned by `DS.belongsTo` as the value. + didDefineProperty: function(proto, key, value) { + // Check if the value being set is a computed property. + if (value instanceof Ember.Descriptor) { - App.Comment = DS.Model.extend({ - post: DS.belongsTo('App.Post') - }); + // If it is, get the metadata for the relationship. This is + // populated by the `DS.belongsTo` helper when it is creating + // the computed property. + var meta = value.meta(); - If the `post` property of a `Comment` was modified, Ember Data would invoke - this helper like this: + if (meta.isRelationship && meta.kind === 'belongsTo') { + Ember.addObserver(proto, key, null, 'belongsToDidChange'); + Ember.addBeforeObserver(proto, key, null, 'belongsToWillChange'); + } - DS._inverseNameFor(App.Comment, App.Post, 'hasMany'); - //=> 'comments' + if (meta.isAttribute) { + Ember.addObserver(proto, key, null, 'attributeDidChange'); + Ember.addBeforeObserver(proto, key, null, 'attributeWillChange'); + } - 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]; -}; + meta.parentType = proto.constructor; + } + } +}); /** - @private + These DS.Model extensions add class methods that provide relationship + introspection abilities about relationships. - Given a model and a relationship name, returns the model type of - the named relationship. + A note about the computed properties contained here: - App.Post = DS.Model.extend({ - comments: DS.hasMany('App.Comment') - }); + **These properties are effectively sealed once called for the first time.** + To avoid repeatedly doing expensive iteration over a model's fields, these + values are computed once and then cached for the remainder of the runtime of + your application. - DS._inverseTypeFor(App.Post, 'comments'); - //=> App.Comment - @param {DS.Model class} modelType - @param {String} relationshipName - @return {DS.Model class} + If your application needs to modify a class after its initial definition + (for example, using `reopen()` to add additional attributes), make sure you + do it before using your model with the store, which uses these properties + extensively. */ -DS._inverseTypeFor = function(modelType, relationshipName) { - var relationships = get(modelType, 'relationshipsByName'), - relationship = relationships.get(relationshipName); - - if (relationship) { return relationship.type; } -}; - -})(); - +DS.Model.reopenClass({ + /** + For a given relationship name, returns the model type of the relationship. -(function() { -var get = Ember.get, set = Ember.set; -var forEach = Ember.EnumerableUtils.forEach; + For example, if you define a model like this: -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; -}; + App.Post = DS.Model.extend({ + comments: DS.hasMany(App.Comment) + }); -DS.RelationshipChangeAdd = function(options){ - DS.RelationshipChange.call(this, options); -}; + Calling `App.Post.typeForRelationship('comments')` will return `App.Comment`. -DS.RelationshipChangeRemove = function(options){ - DS.RelationshipChange.call(this, options); -}; + @param {String} name the name of the relationship + @return {subclass of DS.Model} the type of the relationship, or undefined + */ + typeForRelationship: function(name) { + var relationship = get(this, 'relationshipsByName').get(name); + return relationship && relationship.type; + }, -/** @private */ -DS.RelationshipChange.create = function(options) { - return new DS.RelationshipChange(options); -}; + inverseFor: function(name) { + var inverseType = this.typeForRelationship(name); -/** @private */ -DS.RelationshipChangeAdd.create = function(options) { - return new DS.RelationshipChangeAdd(options); -}; + if (!inverseType) { return null; } -/** @private */ -DS.RelationshipChangeRemove.create = function(options) { - return new DS.RelationshipChangeRemove(options); -}; + var options = this.metaForProperty(name).options; + var inverseName, inverseKind; -DS.OneToManyChange = {}; -DS.OneToNoneChange = {}; -DS.ManyToNoneChange = {}; -DS.OneToOneChange = {}; -DS.ManyToManyChange = {}; + if (options.inverse) { + inverseName = options.inverse; + inverseKind = Ember.get(inverseType, 'relationshipsByName').get(inverseName).kind; + } else { + var possibleRelationships = findPossibleInverses(this, inverseType); -DS.RelationshipChange._createChange = function(options){ - if(options.changeType === "add"){ - return DS.RelationshipChangeAdd.create(options); - } - if(options.changeType === "remove"){ - return DS.RelationshipChangeRemove.create(options); - } -}; + 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); -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"; + inverseName = possibleRelationships[0].name; + inverseKind = possibleRelationships[0].kind; } - } - -}; -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); - } -}; + function findPossibleInverses(type, inverseType, possibleRelationships) { + possibleRelationships = possibleRelationships || []; -/** @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" - }); + var relationshipMap = get(inverseType, 'relationships'); + if (!relationshipMap) { return; } - store.addRelationshipChangeFor(childReference, key, parentReference, null, change); + var relationships = relationshipMap.get(type); + if (relationships) { + possibleRelationships.push.apply(possibleRelationships, relationshipMap.get(type)); + } - return change; -}; + if (type.superclass) { + findPossibleInverses(type.superclass, inverseType, possibleRelationships); + } -/** @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" - }); + return possibleRelationships; + } - store.addRelationshipChangeFor(childReference, key, parentReference, null, change); - return change; -}; + return { + type: inverseType, + name: inverseName, + kind: inverseKind + }; + }, + /** + The model's relationships as a map, keyed on the type of the + relationship. The value of each entry is an array containing a descriptor + for each relationship with that type, describing the name of the relationship + as well as the type. -/** @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; + For example, given the following model definition: - // 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; + App.Blog = DS.Model.extend({ + users: DS.hasMany(App.User), + owner: DS.belongsTo(App.User), + posts: DS.hasMany(App.Post) + }); - var change = DS.RelationshipChange._createChange({ - parentReference: parentReference, - childReference: childReference, - firstRecordReference: childReference, - secondRecordReference: parentReference, - firstRecordKind: "hasMany", - secondRecordKind: "hasMany", - store: store, - changeType: options.changeType, - firstRecordName: key - }); + This computed property would return a map describing these + relationships, like this: - store.addRelationshipChangeFor(childReference, key, parentReference, null, change); + var relationships = Ember.get(App.Blog, 'relationships'); + relationships.get(App.User); + //=> [ { name: 'users', kind: 'hasMany' }, + // { name: 'owner', kind: 'belongsTo' } ] + relationships.get(App.Post); + //=> [ { name: 'posts', kind: 'hasMany' } ] + @type Ember.Map + @readOnly + */ + relationships: Ember.computed(function() { + var map = new Ember.MapWithDefault({ + defaultValue: function() { return []; } + }); - return change; -}; + // Loop through each computed property on the class + this.eachComputedProperty(function(name, meta) { -/** @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 computed property is a relationship, add + // it to the map. + if (meta.isRelationship) { + if (typeof meta.type === 'string') { + meta.type = Ember.get(Ember.lookup, meta.type); + } - // 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 relationshipsForType = map.get(meta.type); - var change = DS.RelationshipChange._createChange({ - parentReference: parentReference, - childReference: childReference, - firstRecordReference: childReference, - secondRecordReference: parentReference, - firstRecordKind: "belongsTo", - secondRecordKind: "belongsTo", - store: store, - changeType: options.changeType, - firstRecordName: key - }); + relationshipsForType.push({ name: name, kind: meta.kind }); + } + }); - store.addRelationshipChangeFor(childReference, key, parentReference, null, change); + return map; + }), + /** + A hash containing lists of the model's relationships, grouped + by the relationship kind. For example, given a model with this + definition: - return change; -}; + App.Blog = DS.Model.extend({ + users: DS.hasMany(App.User), + owner: DS.belongsTo(App.User), -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 + posts: DS.hasMany(App.Post) }); - 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; + This property would contain the following: - // If the name of the belongsTo side of the relationship is specified, - // use that - // If the type of the parent is specified, look it up on the child's type - // definition. - if (options.parentType) { - key = inverseBelongsToName(options.parentType, childType, options.key); - DS.OneToManyChange.maintainInvariant( options, store, childReference, key ); - } else if (options.key) { - key = options.key; - } else { - Ember.assert("You must pass either a parentType or belongsToName option to OneToManyChange.forChildAndParent", false); - } + var relationshipNames = Ember.get(App.Blog, 'relationshipNames'); + relationshipNames.hasMany; + //=> ['users', 'posts'] + relationshipNames.belongsTo; + //=> ['owner'] - var change = DS.RelationshipChange._createChange({ - parentReference: parentReference, - childReference: childReference, - firstRecordReference: childReference, - secondRecordReference: parentReference, - firstRecordKind: "belongsTo", - secondRecordKind: "hasMany", - store: store, - changeType: options.changeType, - firstRecordName: key - }); + @type Object + @readOnly + */ + relationshipNames: Ember.computed(function() { + var names = { hasMany: [], belongsTo: [] }; - store.addRelationshipChangeFor(childReference, key, parentReference, null, change); + this.eachComputedProperty(function(name, meta) { + if (meta.isRelationship) { + names[meta.kind].push(name); + } + }); + return names; + }), - return change; -}; + /** + An array of types directly related to a model. Each type will be + included once, regardless of the number of relationships it has with + the model. + For example, given a model with this definition: -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 + App.Blog = DS.Model.extend({ + users: DS.hasMany(App.User), + owner: DS.belongsTo(App.User), + posts: DS.hasMany(App.Post) }); - store.addRelationshipChangeFor(childReference, key, options.parentReference , null, correspondingChange); - correspondingChange.sync(); - } - } -}; -DS.OneToManyChange.ensureSameTransaction = function(changes, store){ - var records = Ember.A(); - forEach(changes, function(change){ - records.addObject(change.getSecondRecord()); - records.addObject(change.getFirstRecord()); - }); - var transaction = store.ensureSameTransaction(records); - forEach(changes, function(change){ - change.transaction = transaction; - }); -}; + This property would contain the following: -DS.RelationshipChange.prototype = { + var relatedTypes = Ember.get(App.Blog, 'relatedTypes'); + //=> [ App.User, App.Post ] - getSecondRecordName: function() { - var name = this.secondRecordName, store = this.store, parent; + @type Ember.Array + @readOnly + */ + relatedTypes: Ember.computed(function() { + var type, + types = Ember.A([]); - if (!name) { - parent = this.secondRecordReference; - if (!parent) { return; } + // Loop through each computed property on the class, + // and create an array of the unique types involved + // in relationships + this.eachComputedProperty(function(name, meta) { + if (meta.isRelationship) { + type = meta.type; - var childType = this.firstRecordReference.type; - var inverseType = DS._inverseTypeFor(childType, this.firstRecordName); - name = inverseHasManyName(inverseType, childType, this.firstRecordName); - this.secondRecordName = name; - } + if (typeof type === 'string') { + type = get(this, type, false) || get(Ember.lookup, type); + } - return name; - }, + Ember.assert("You specified a hasMany (" + meta.type + ") on " + meta.parentType + " but " + meta.type + " was not found.", type); + + if (!types.contains(type)) { + Ember.assert("Trying to sideload " + name + " on " + this.toString() + " but the type doesn't exist.", !!type); + types.push(type); + } + } + }); + + return types; + }), /** - Get the name of the relationship on the belongsTo side. + A map whose keys are the relationships of a model and whose values are + relationship descriptors. - @returns {String} - */ - getFirstRecordName: function() { - var name = this.firstRecordName, store = this.store, parent, child; + For example, given a model with this + definition: - if (!name) { - parent = this.secondRecordReference; - child = this.firstRecordReference; - if (!(child && parent)) { return; } + App.Blog = DS.Model.extend({ + users: DS.hasMany(App.User), + owner: DS.belongsTo(App.User), - name = DS._inverseRelationshipFor(child.type, parent.type).name; + posts: DS.hasMany(App.Post) + }); - this.firstRecordName = name; - } + This property would contain the following: - return name; - }, + var relationshipsByName = Ember.get(App.Blog, 'relationshipsByName'); + relationshipsByName.get('users'); + //=> { key: 'users', kind: 'hasMany', type: App.User } + relationshipsByName.get('owner'); + //=> { key: 'owner', kind: 'belongsTo', type: App.User } - /** @private */ - destroy: function() { - var childReference = this.childReference, - belongsToName = this.getFirstRecordName(), - hasManyName = this.getSecondRecordName(), - store = this.store, - child, oldParent, newParent, lastParent, transaction; + @type Ember.Map + @readOnly + */ + relationshipsByName: Ember.computed(function() { + var map = Ember.Map.create(), type; - store.removeRelationshipChangeFor(childReference, belongsToName, this.parentReference, hasManyName, this.changeType); + this.eachComputedProperty(function(name, meta) { + if (meta.isRelationship) { + meta.key = name; + type = meta.type; - if (transaction = this.transaction) { - transaction.relationshipBecameClean(this); - } - }, + if (typeof type === 'string') { + type = get(this, type, false) || get(Ember.lookup, type); + meta.type = type; + } - /** @private */ - getByReference: function(reference) { - var store = this.store; + map.set(name, meta); + } + }); - // return null or undefined if the original reference was null or undefined - if (!reference) { return reference; } + return map; + }), - if (store.recordIsMaterialized(reference)) { - return store.recordForReference(reference); - } - }, + /** + A map whose keys are the fields of the model and whose values are strings + describing the kind of the field. A model's fields are the union of all of its + attributes and relationships. - getSecondRecord: function(){ - return this.getByReference(this.secondRecordReference); - }, + For example: - /** @private */ - getFirstRecord: function() { - return this.getByReference(this.firstRecordReference); - }, + App.Blog = DS.Model.extend({ + users: DS.hasMany(App.User), + owner: DS.belongsTo(App.User), - /** - @private + posts: DS.hasMany(App.Post), - 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(); + title: DS.attr('string') + }); - var transaction = this.store.ensureSameTransaction([child, parentRecord]); + var fields = Ember.get(App.Blog, 'fields'); + fields.forEach(function(field, kind) { + console.log(field, kind); + }); - this.transaction = transaction; - return transaction; - }, + // prints: + // users, hasMany + // owner, belongsTo + // posts, hasMany + // title, attribute - callChangeEvents: function(){ - var hasManyName = this.getSecondRecordName(), - belongsToName = this.getFirstRecordName(), - child = this.getFirstRecord(), - parentRecord = this.getSecondRecord(); + @type Ember.Map + @readOnly + */ + fields: Ember.computed(function() { + var map = Ember.Map.create(); - var dirtySet = new Ember.OrderedSet(); + this.eachComputedProperty(function(name, meta) { + if (meta.isRelationship) { + map.set(name, meta.kind); + } else if (meta.isAttribute) { + map.set(name, 'attribute'); + } + }); - // 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); - } + return map; + }), - if (child) { - this.store.recordBelongsToDidChange(dirtySet, child, this); - } + /** + Given a callback, iterates over each of the relationships in the model, + invoking the callback with the name of each relationship and its relationship + descriptor. - dirtySet.forEach(function(record) { - record.adapterDidDirty(); + @param {Function} callback the callback to invoke + @param {any} binding the value to which the callback's `this` should be bound + */ + eachRelationship: function(callback, binding) { + get(this, 'relationshipsByName').forEach(function(name, relationship) { + callback.call(binding, name, relationship); }); }, - 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(); - } + /** + Given a callback, iterates over each of the types related to a model, + invoking the callback with the related type's class. Each type will be + returned just once, regardless of how many different relationships it has + with a model. + + @param {Function} callback the callback to invoke + @param {any} binding the value to which the callback's `this` should be bound + */ + eachRelatedType: function(callback, binding) { + get(this, 'relatedTypes').forEach(function(type) { + callback.call(binding, type); }); } -}; +}); -DS.RelationshipChangeAdd.prototype = Ember.create(DS.RelationshipChange.create({})); -DS.RelationshipChangeRemove.prototype = Ember.create(DS.RelationshipChange.create({})); +DS.Model.reopen({ + /** + Given a callback, iterates over each of the relationships in the model, + invoking the callback with the name of each relationship and its relationship + descriptor. -DS.RelationshipChangeAdd.prototype.changeType = "add"; -DS.RelationshipChangeAdd.prototype.sync = function() { - var secondRecordName = this.getSecondRecordName(), - firstRecordName = this.getFirstRecordName(), - firstRecord = this.getFirstRecord(), - secondRecord = this.getSecondRecord(); + @param {Function} callback the callback to invoke + @param {any} binding the value to which the callback's `this` should be bound + */ + eachRelationship: function(callback, binding) { + this.constructor.eachRelationship(callback, binding); + } +}); - //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); - }); +(function() { +/** + @module data + @submodule data-relationships +*/ - } - else if(this.secondRecordKind === "hasMany"){ - secondRecord.suspendRelationshipObservers(function(){ - get(secondRecord, secondRecordName).addObject(firstRecord); - }); - } - } +})(); - if (firstRecord && secondRecord && get(firstRecord, firstRecordName) !== secondRecord) { - if(this.firstRecordKind === "belongsTo"){ - firstRecord.suspendRelationshipObservers(function(){ - set(firstRecord, firstRecordName, secondRecord); - }); - } - else if(this.firstdRecordKind === "hasMany"){ - firstRecord.suspendRelationshipObservers(function(){ - get(firstRecord, firstRecordName).addObject(secondRecord); - }); - } - } - this.coalesce(); -}; -DS.RelationshipChangeRemove.prototype.changeType = "remove"; -DS.RelationshipChangeRemove.prototype.sync = function() { - var secondRecordName = this.getSecondRecordName(), - firstRecordName = this.getFirstRecordName(), - firstRecord = this.getFirstRecord(), - secondRecord = this.getSecondRecord(); +(function() { +var get = Ember.get, set = Ember.set; +var once = Ember.run.once; +var forEach = Ember.EnumerableUtils.forEach; - //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); +DS.RecordArrayManager = Ember.Object.extend({ + init: function() { + this.filteredRecordArrays = Ember.MapWithDefault.create({ + defaultValue: function() { return []; } + }); - var transaction = this.ensureSameTransaction(firstRecord, secondRecord, secondRecordName, firstRecordName); - transaction.relationshipBecameDirty(this); + this.changedReferences = []; + }, - this.callChangeEvents(); + referenceDidChange: function(reference) { + this.changedReferences.push(reference); + once(this, this.updateRecordArrays); + }, - 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); - }); - } - } + recordArraysForReference: function(reference) { + reference.recordArrays = reference.recordArrays || Ember.OrderedSet.create(); + return reference.recordArrays; + }, - 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); - }); - } - } + /** + @private - this.coalesce(); -}; + 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. -function inverseBelongsToName(parentType, childType, hasManyName) { - // Get the options passed to the parent's DS.hasMany() - var options = parentType.metaForProperty(hasManyName).options; - var belongsToName; + It updates all filters that a record belongs to. - if (belongsToName = options.inverse) { - return belongsToName; - } + To avoid thrashing, it only runs once per run loop per record. - return DS._inverseRelationshipFor(childType, parentType).name; -} + @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); -function inverseHasManyName(parentType, childType, belongsToName) { - var options = childType.metaForProperty(belongsToName).options; - var hasManyName; + // loop through all manyArrays containing an unloaded copy of this + // clientId and notify them that the record was loaded. + var manyArrays = reference.loadingRecordArrays; - if (hasManyName = options.inverse) { - return hasManyName; - } + if (manyArrays) { + for (var i=0, l=manyArrays.length; i 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 (since = meta[this.configOption(type, 'since')]) { - loader.sinceForType(type, since); + if(meta && json[meta]){ + data = json[meta]; } + + this.metadataMapping.forEach(function(property, key){ + value = data[property]; + if(!Ember.isNone(value)){ + loader.metaForType(type, key, value); + } + }); + }, + + extractEmbeddedType: function(relationship, data) { + var foundType = relationship.type; + if(relationship.options && relationship.options.polymorphic) { + var key = this.keyFor(relationship), + keyForEmbeddedType = this.keyForEmbeddedType(key); + + foundType = this.typeFromAlias(data[keyForEmbeddedType]); + delete data[keyForEmbeddedType]; + } + + return foundType; }, /** @@ -6590,45 +7166,22 @@ 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]); } }, - /** - @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 @@ -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(); @@ -6715,29 +7277,13 @@ DS.JSONSerializer = DS.Serializer.extend({ return name.replace(/([A-Z])/g, '_$1').toLowerCase().slice(1); }, - /** - @private - - Determines the root name mapped to a particular sideloaded type. - - @param {DS.Model subclass} type - @returns {String} name of the root element, if any is registered - */ - sideloadMappingForType: function(type) { - this.sideloadMapping.forEach(function(key, value) { - if (type === value) { - return key; - } - }); - }, - /** @private The default root name for a particular sideloaded type. @param {DS.Model subclass} type - @returns {String} name of the root element + @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({ @@ -7489,21 +8095,9 @@ DS.FixtureSerializer = DS.Serializer.extend({ addHasMany: function(hash, record, key, relationship) { var ids = get(record, relationship.key).map(function(item) { return item.get('id'); - }); - - hash[relationship.key] = ids; - }, - - /** - @private - - Creates an empty hash that will be filled in by the hooks called from the - `serialize()` method. - - @return {Object} - */ - createSerializedForm: function() { - return {}; + }); + + hash[relationship.key] = ids; }, extract: function(loader, fixture, type, record) { @@ -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 +*/ + })(); + +})(); -- cgit v1.2.3