From 57074a463626bb93abbab951ef51201571f11789 Mon Sep 17 00:00:00 2001 From: IgorT Date: Sun, 12 Apr 2015 12:50:02 +0200 Subject: [PATCH] Refactor internals to use Javascript objects for internals instead of DS.Model This commit adds an `InternalModel` class that is now used everywhere inside ED to represent models. This class is a fast Javascript object that contains all the data that we know for a particular record. At the ED/App code boundaries, such as responses to `find`, models being set/pushed to `belongsTo`/`hasMany` we convert between internalModels and DS.Models. This should be a huge performance win, because we now lazily create DS.Models which are pretty slow to instantiate. We need to wait for the new serializer/push refactor in order to use a `push` that does not immediately materialize a record to get further perf gains. For now most of perf gains if for foreign keys. --- packages/ember-data/lib/system/many-array.js | 13 +- .../ember-data/lib/system/model/attributes.js | 14 +- .../lib/system/model/internal-model.js | 696 ++++++++++++++++++ packages/ember-data/lib/system/model/model.js | 537 +------------- .../ember-data/lib/system/model/states.js | 20 +- .../lib/system/record-array-manager.js | 10 +- .../adapter-populated-record-array.js | 6 +- .../lib/system/record-arrays/record-array.js | 6 +- .../lib/system/relationships/belongs-to.js | 10 +- .../lib/system/relationships/has-many.js | 7 +- .../system/relationships/state/belongs-to.js | 18 +- .../lib/system/relationships/state/create.js | 2 +- .../system/relationships/state/has-many.js | 15 +- .../relationships/state/relationship.js | 6 +- packages/ember-data/lib/system/snapshot.js | 18 +- packages/ember-data/lib/system/store.js | 177 +++-- .../ember-data/lib/system/store/finders.js | 20 +- .../integration/adapter/rest-adapter-test.js | 2 +- .../integration/adapter/store-adapter-test.js | 2 +- .../tests/integration/lifecycle-hooks-test.js | 4 +- .../integration/record-array-manager-test.js | 6 +- .../tests/integration/records/save-test.js | 15 +- .../relationships/belongs-to-test.js | 12 +- .../relationships/has-many-test.js | 18 +- packages/ember-data/tests/unit/model-test.js | 12 +- .../unit/model/lifecycle-callbacks-test.js | 26 + .../model/relationships/record-array-test.js | 7 +- .../tests/unit/store/unload-test.js | 4 +- 28 files changed, 1013 insertions(+), 670 deletions(-) create mode 100644 packages/ember-data/lib/system/model/internal-model.js diff --git a/packages/ember-data/lib/system/many-array.js b/packages/ember-data/lib/system/many-array.js index be0ff78d696..b9fbcf89816 100644 --- a/packages/ember-data/lib/system/many-array.js +++ b/packages/ember-data/lib/system/many-array.js @@ -6,6 +6,7 @@ import { PromiseArray } from "ember-data/system/promise-proxies"; var get = Ember.get; var set = Ember.set; var filter = Ember.ArrayPolyfills.filter; +var map = Ember.EnumerableUtils.map; /** A `ManyArray` is a `MutableArray` that represents the contents of a has-many @@ -57,19 +58,23 @@ export default Ember.Object.extend(Ember.MutableArray, Ember.Evented, { length: 0, objectAt: function(index) { - return this.currentState[index]; + //Ember observers such as 'firstObject', 'lastObject' might do out of bounds accesses + if (!this.currentState[index]) { + return undefined; + } + return this.currentState[index].getRecord(); }, flushCanonical: function() { //TODO make this smarter, currently its plenty stupid var toSet = filter.call(this.canonicalState, function(record) { - return !record.get('isDeleted'); + return !record.isDeleted(); }); //a hack for not removing new records //TODO remove once we have proper diffing var newRecords = this.currentState.filter(function(record) { - return record.get('isNew'); + return record.isNew(); }); toSet = toSet.concat(newRecords); var oldLength = this.length; @@ -143,7 +148,7 @@ export default Ember.Object.extend(Ember.MutableArray, Ember.Evented, { this.get('relationship').removeRecords(records); } if (objects) { - this.get('relationship').addRecords(objects, idx); + this.get('relationship').addRecords(map(objects, function(obj) { return obj._internalModel; }), idx); } }, /** diff --git a/packages/ember-data/lib/system/model/attributes.js b/packages/ember-data/lib/system/model/attributes.js index 505d4bd38bd..ff232ffce6e 100644 --- a/packages/ember-data/lib/system/model/attributes.js +++ b/packages/ember-data/lib/system/model/attributes.js @@ -300,25 +300,27 @@ export default function attr(type, options) { return computedPolyfill({ get: function(key) { - if (hasValue(this, key)) { - return getValue(this, key); + var internalModel = this._internalModel; + if (hasValue(internalModel, key)) { + return getValue(internalModel, key); } else { return getDefaultValue(this, options, key); } }, set: function(key, value) { Ember.assert("You may not set `id` as an attribute on your model. Please remove any lines that look like: `id: DS.attr('')` from " + this.constructor.toString(), key !== 'id'); - var oldValue = getValue(this, key); + var internalModel = this._internalModel; + var oldValue = getValue(internalModel, key); if (value !== oldValue) { // Add the new value to the changed attributes hash; it will get deleted by // the 'didSetProperty' handler if it is no different from the original value - this._attributes[key] = value; + internalModel._attributes[key] = value; - this.send('didSetProperty', { + this._internalModel.send('didSetProperty', { name: key, oldValue: oldValue, - originalValue: this._data[key], + originalValue: internalModel._data[key], value: value }); } diff --git a/packages/ember-data/lib/system/model/internal-model.js b/packages/ember-data/lib/system/model/internal-model.js new file mode 100644 index 00000000000..d9b3475c724 --- /dev/null +++ b/packages/ember-data/lib/system/model/internal-model.js @@ -0,0 +1,696 @@ +import merge from "ember-data/system/merge"; +import RootState from "ember-data/system/model/states"; +import createRelationshipFor from "ember-data/system/relationships/state/create"; +import Snapshot from "ember-data/system/snapshot"; +import Errors from "ember-data/system/model/errors"; + +var Promise = Ember.RSVP.Promise; +var get = Ember.get; +var set = Ember.set; +var forEach = Ember.ArrayPolyfills.forEach; +var map = Ember.ArrayPolyfills.map; + +var _extractPivotNameCache = Ember.create(null); +var _splitOnDotCache = Ember.create(null); + +function splitOnDot(name) { + return _splitOnDotCache[name] || ( + _splitOnDotCache[name] = name.split('.') + ); +} + +function extractPivotName(name) { + return _extractPivotNameCache[name] || ( + _extractPivotNameCache[name] = splitOnDot(name)[0] + ); +} + +function retrieveFromCurrentState(key) { + return function() { + return get(this.currentState, key); + }; +} + +/* + `InternalModel` is the Model class that we use internally inside Ember Data to represent models. + Internal ED methods should only deal with `InternalModel` objects. It is a fast, plain Javascript class. + + We expose `DS.Model` to application code, by materializing a `DS.Model` from `InternalModel` lazily, as + a performance optimization. + + `InternalModel` should never be exposed to application code. At the boundaries of the system, in places + like `find`, `push`, etc. we convert between Models and InternalModels. + + We need to make sure that the properties from `InternalModel` are correctly exposed/proxied on `Model` + if they are needed. +*/ + +var InternalModel = function(type, id, store, container, data) { + this.type = type; + this.id = id; + this.store = store; + this.container = container; + this._data = data || {}; + this.modelName = type.modelName; + this.errors = null; + this.dataHasInitialized = false; + this._deferredTriggers = []; + this._data = {}; + this._attributes = Ember.create(null); + this._inFlightAttributes = Ember.create(null); + this._relationships = {}; + this.currentState = RootState.empty; + /* + implicit relationships are relationship which have not been declared but the inverse side exists on + another record somewhere + For example if there was + ``` + App.Comment = DS.Model.extend({ + name: DS.attr() + }) + ``` + but there is also + ``` + App.Post = DS.Model.extend({ + name: DS.attr(), + comments: DS.hasMany('comment') + }) + ``` + + would have a implicit post relationship in order to be do things like remove ourselves from the post + when we are deleted + */ + this._implicitRelationships = Ember.create(null); + var model = this; + //TODO Move into a getter for better perf + this.eachRelationship(function(key, descriptor) { + model._relationships[key] = createRelationshipFor(model, descriptor, model.store); + }); +}; + +InternalModel.prototype = { + isEmpty: retrieveFromCurrentState('isEmpty'), + isLoading: retrieveFromCurrentState('isLoading'), + isLoaded: retrieveFromCurrentState('isLoaded'), + isDirty: retrieveFromCurrentState('isDirty'), + isSaving: retrieveFromCurrentState('isSaving'), + isDeleted: retrieveFromCurrentState('isDeleted'), + isNew: retrieveFromCurrentState('isNew'), + isValid: retrieveFromCurrentState('isValid'), + dirtyType: retrieveFromCurrentState('dirtyType'), + + constructor: InternalModel, + materializeRecord: function() { + // lookupFactory should really return an object that creates + // instances with the injections applied + this.record = this.type._create({ + id: this.id, + store: this.store, + container: this.container + }); + this.record._internalModel = this; + this._triggerDeferredTriggers(); + }, + + recordObjectWillDestroy: function() { + this.record = null; + }, + + + deleteRecord: function() { + this.send('deleteRecord'); + }, + + save: function() { + var promiseLabel = "DS: Model#save " + this; + var resolver = Ember.RSVP.defer(promiseLabel); + + this.store.scheduleSave(this, resolver); + return resolver.promise; + }, + + startedReloading: function() { + this.isReloading = true; + if (this.record) { + set(this.record, 'isReloading', true); + } + }, + + finishedReloading: function() { + this.isReloading = false; + if (this.record) { + set(this.record, 'isReloading', false); + } + }, + + reload: function() { + this.startedReloading(); + var record = this; + var promiseLabel = "DS: Model#reload of " + this; + return new Promise(function(resolve) { + record.send('reloadRecord', resolve); + }, promiseLabel).then(function() { + record.didCleanError(); + return record; + }, function(reason) { + record.didError(); + throw reason; + }, "DS: Model#reload complete, update flags")['finally'](function () { + record.finishedReloading(); + record.updateRecordArrays(); + }); + }, + + getRecord: function() { + if (!this.record) { + this.materializeRecord(); + } + return this.record; + }, + + unloadRecord: function() { + this.send('unloadRecord'); + }, + + eachRelationship: function(callback, binding) { + return this.type.eachRelationship(callback, binding); + }, + + eachAttribute: function(callback, binding) { + return this.type.eachAttribute(callback, binding); + }, + + inverseFor: function(key) { + return this.type.inverseFor(key); + }, + + setupData: function(data) { + var changedKeys = mergeAndReturnChangedKeys(this._data, data); + this.pushedData(); + if (this.record) { + this.record._notifyProperties(changedKeys); + } + this.didInitalizeData(); + }, + + becameReady: function() { + var self = this; + Ember.run.schedule('actions', function() { + self.store.recordArrayManager.recordWasLoaded(self); + }); + }, + + didInitalizeData: function() { + if (!this.dataHasInitialized) { + this.becameReady(); + this.dataHasInitialized = true; + } + }, + + destroy: function() { + if (this.record) { + return this.record.destroy(); + } + }, + + /** + @method _createSnapshot + @private + */ + _createSnapshot: function() { + return new Snapshot(this); + }, + + /** + @method loadingData + @private + @param {Promise} promise + */ + loadingData: function(promise) { + this.send('loadingData', promise); + }, + + /** + @method loadedData + @private + */ + loadedData: function() { + this.send('loadedData'); + this.didInitalizeData(); + }, + + /** + @method notFound + @private + */ + notFound: function() { + this.send('notFound'); + }, + + /** + @method pushedData + @private + */ + pushedData: function() { + this.send('pushedData'); + }, + + flushChangedAttributes: function() { + this._inFlightAttributes = this._attributes; + this._attributes = Ember.create(null); + }, + + /** + @method adapterWillCommit + @private + */ + adapterWillCommit: function() { + this.flushChangedAttributes(); + this.send('willCommit'); + }, + + /** + @method adapterDidDirty + @private + */ + adapterDidDirty: function() { + this.send('becomeDirty'); + this.updateRecordArraysLater(); + }, + + /** + @method send + @private + @param {String} name + @param {Object} context + */ + send: function(name, context) { + var currentState = get(this, 'currentState'); + + if (!currentState[name]) { + this._unhandledEvent(currentState, name, context); + } + + return currentState[name](this, context); + }, + + notifyHasManyAdded: function(key, record, idx) { + if (this.record) { + this.record.notifyHasManyAdded(key, record, idx); + } + }, + + notifyHasManyRemoved: function(key, record, idx) { + if (this.record) { + this.record.notifyHasManyRemoved(key, record, idx); + } + }, + + notifyBelongsToChanged: function(key, record) { + if (this.record) { + this.record.notifyBelongsToChanged(key, record); + } + }, + + notifyPropertyChange: function(key) { + if (this.record) { + this.record.notifyPropertyChange(key); + } + }, + + rollback: function() { + var dirtyKeys = Ember.keys(this._attributes); + + this._attributes = Ember.create(null); + + if (get(this, 'isError')) { + this._inFlightAttributes = Ember.create(null); + this.didCleanError(); + } + + //Eventually rollback will always work for relationships + //For now we support it only out of deleted state, because we + //have an explicit way of knowing when the server acked the relationship change + if (this.isDeleted()) { + //TODO: Should probably move this to the state machine somehow + this.becameReady(); + this.reconnectRelationships(); + } + + if (this.isNew()) { + this.clearRelationships(); + } + + if (this.isValid()) { + this._inFlightAttributes = Ember.create(null); + } + + this.send('rolledBack'); + + this.record._notifyProperties(dirtyKeys); + + }, + /** + @method transitionTo + @private + @param {String} name + */ + transitionTo: function(name) { + // POSSIBLE TODO: Remove this code and replace with + // always having direct reference to state objects + + var pivotName = extractPivotName(name); + var currentState = get(this, 'currentState'); + var state = currentState; + + do { + if (state.exit) { state.exit(this); } + state = state.parentState; + } while (!state.hasOwnProperty(pivotName)); + + var path = splitOnDot(name); + var setups = []; + var enters = []; + var i, l; + + for (i=0, l=path.length; i model.getRecord()); + } + return promiseObject(toReturn, label); +} + var get = Ember.get; var set = Ember.set; var once = Ember.run.once; @@ -113,7 +129,7 @@ if (!Service) { // * +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 +// * +internalModel+ means a record internalModel object, which holds metadata about a // record, even if it has not yet been fully materialized. // * +type+ means a subclass of DS.Model. @@ -321,17 +337,18 @@ Store = Service.extend({ // Coerce ID to a string properties.id = coerceId(properties.id); - var record = this.buildRecord(typeClass, properties.id); + var internalModel = this.buildInternalModel(typeClass, properties.id); + var record = internalModel.getRecord(); // Move the record out of its initial `empty` state into // the `loaded` state. - record.loadedData(); + internalModel.loadedData(); // Set the properties specified on the record. record.setProperties(properties); - record.eachRelationship(function(key, descriptor) { - record._relationships[key].setHasData(true); + internalModel.eachRelationship(function(key, descriptor) { + internalModel._relationships[key].setHasData(true); }); return record; @@ -598,12 +615,30 @@ Store = Service.extend({ */ findById: function(modelName, id, preload) { - var typeClass = this.modelFor(modelName); - var record = this.recordForId(typeClass, id); + var type = this.modelFor(modelName); + var internalModel = this._internalModelForId(type, id); - return this._findByRecord(record, preload); + return this._findByRecord(internalModel, preload); }, + _findByInternalModel: function(internalModel, preload) { + var fetchedInternalModel; + + if (preload) { + internalModel._preloadData(preload); + } + + if (internalModel.isEmpty()) { + fetchedInternalModel = this.scheduleFetch(internalModel); + //TODO double check about reloading + } else if (internalModel.isLoading()) { + fetchedInternalModel = internalModel._loadingPromise; + } + + return promiseRecord(fetchedInternalModel || internalModel, "DS: Store#findByRecord " + internalModel.typeKey + " with id: " + get(internalModel, 'id')); + }, + + _findByRecord: function(record, preload) { var fetchedRecord; @@ -611,14 +646,14 @@ Store = Service.extend({ record._preloadData(preload); } - if (get(record, 'isEmpty')) { + if (record.isEmpty()) { fetchedRecord = this.scheduleFetch(record); //TODO double check about reloading - } else if (get(record, 'isLoading')) { + } else if (record.isLoading()) { fetchedRecord = record._loadingPromise; } - return promiseObject(fetchedRecord || record, "DS: Store#findByRecord " + record.modelName + " with id: " + get(record, 'id')); + return promiseRecord(fetchedRecord || record, "DS: Store#findByRecord " + record.modelName + " with id: " + get(record, 'id')); }, /** @@ -650,7 +685,7 @@ Store = Service.extend({ @return {Promise} promise */ fetchRecord: function(record) { - var typeClass = record.constructor; + var typeClass = record.type; var id = get(record, 'id'); var adapter = this.adapterFor(typeClass); @@ -662,15 +697,17 @@ Store = Service.extend({ }, scheduleFetchMany: function(records) { - return Promise.all(map(records, this.scheduleFetch, this)); + var internalModel = map(records, function(record) { return record._internalModel; }); + return Promise.all(map(internalModel, this.scheduleFetch, this)); }, scheduleFetch: function(record) { - var typeClass = record.constructor; + var typeClass = record.type; + if (isNone(record)) { return null; } if (record._loadingPromise) { return record._loadingPromise; } - var resolver = Ember.RSVP.defer('Fetching ' + typeClass + 'with id: ' + record.get('id')); + var resolver = Ember.RSVP.defer('Fetching ' + typeClass + 'with id: ' + record.id); var recordResolverPair = { record: record, resolver: resolver @@ -766,7 +803,7 @@ Store = Service.extend({ var snapshots = Ember.A(records).invoke('_createSnapshot'); var groups = adapter.groupRecordsForFindMany(this, snapshots); forEach(groups, function (groupOfSnapshots) { - var groupOfRecords = Ember.A(groupOfSnapshots).mapBy('record'); + var groupOfRecords = Ember.A(groupOfSnapshots).mapBy('record._internalModel'); var requestedRecords = Ember.A(groupOfRecords); var ids = requestedRecords.mapBy('id'); if (ids.length > 1) { @@ -808,7 +845,7 @@ Store = Service.extend({ */ getById: function(type, id) { if (this.hasRecordForId(type, id)) { - return this.recordForId(type, id); + return this._internalModelForId(type, id).getRecord(); } else { return null; } @@ -850,7 +887,7 @@ Store = Service.extend({ var typeClass = this.modelFor(modelName); var id = coerceId(inputId); var record = this.typeMapFor(typeClass).idToRecord[id]; - return !!record && get(record, 'isLoaded'); + return !!record && record.isLoaded(); }, /** @@ -863,19 +900,25 @@ Store = Service.extend({ @param {String|Integer} id @return {DS.Model} record */ - recordForId: function(modelName, inputId) { - var typeClass = this.modelFor(modelName); + recordForId: function(modelName, id) { + return this._internalModelForId(modelName, id).getRecord(); + }, + + _internalModelForId: function(typeName, inputId) { + var typeClass = this.modelFor(typeName); var id = coerceId(inputId); var idToRecord = this.typeMapFor(typeClass).idToRecord; var record = idToRecord[id]; if (!record || !idToRecord[id]) { - record = this.buildRecord(typeClass, id); + record = this.buildInternalModel(typeClass, id); } return record; }, + + /** @method findMany @private @@ -912,9 +955,9 @@ Store = Service.extend({ @return {Promise} promise */ findHasMany: function(owner, link, type) { - var adapter = this.adapterFor(owner.constructor); + var adapter = this.adapterFor(owner.type); - Ember.assert("You tried to load a hasMany relationship but you have no adapter (for " + owner.constructor + ")", adapter); + Ember.assert("You tried to load a hasMany relationship but you have no adapter (for " + owner.type + ")", adapter); Ember.assert("You tried to load a hasMany relationship from a specified `link` in the original payload but your adapter does not implement `findHasMany`", typeof adapter.findHasMany === 'function'); return _findHasMany(adapter, this, owner, link, type); @@ -929,9 +972,9 @@ Store = Service.extend({ @return {Promise} promise */ findBelongsTo: function(owner, link, relationship) { - var adapter = this.adapterFor(owner.constructor); + var adapter = this.adapterFor(owner.type); - Ember.assert("You tried to load a belongsTo relationship but you have no adapter (for " + owner.constructor + ")", adapter); + Ember.assert("You tried to load a belongsTo relationship but you have no adapter (for " + owner.type + ")", adapter); Ember.assert("You tried to load a belongsTo relationship from a specified `link` in the original payload but your adapter does not implement `findBelongsTo`", typeof adapter.findBelongsTo === 'function'); return _findBelongsTo(adapter, this, owner, link, relationship); @@ -1190,8 +1233,7 @@ Store = Service.extend({ @return {boolean} */ recordIsLoaded: function(type, id) { - if (!this.hasRecordForId(type, id)) { return false; } - return !get(this.recordForId(type, id), 'isEmpty'); + return this.hasRecordForId(type, id); }, /** @@ -1274,15 +1316,15 @@ Store = Service.extend({ forEach(pending, function(tuple) { var snapshot = tuple[0]; var resolver = tuple[1]; - var record = snapshot.record; - var adapter = this.adapterFor(record.constructor); + var record = snapshot._internalModel; + var adapter = this.adapterFor(record.type); var operation; if (get(record, 'currentState.stateName') === 'root.deleted.saved') { - return resolver.resolve(record); - } else if (get(record, 'isNew')) { + return resolver.resolve(); + } else if (record.isNew()) { operation = 'createRecord'; - } else if (get(record, 'isDeleted')) { + } else if (record.isDeleted()) { operation = 'deleteRecord'; } else { operation = 'updateRecord'; @@ -1308,7 +1350,7 @@ Store = Service.extend({ didSaveRecord: function(record, data) { if (data) { // normalize relationship IDs into records - this._backburner.schedule('normalizeRelationships', this, '_setupRelationships', record, record.constructor, data); + this._backburner.schedule('normalizeRelationships', this, '_setupRelationships', record, record.type, data); this.updateId(record, data); } @@ -1360,9 +1402,9 @@ Store = Service.extend({ Ember.assert("An adapter cannot assign a new id to a record that already has an id. " + record + " had id: " + oldId + " and you tried to update it with " + id + ". This likely happened because your server returned data in response to a find or update that had a different id than the one you sent.", oldId === null || id === oldId); - this.typeMapFor(record.constructor).idToRecord[id] = record; + this.typeMapFor(record.type).idToRecord[id] = record; - set(record, 'id', id); + record.setId(id); }, /** @@ -1406,12 +1448,13 @@ Store = Service.extend({ */ _load: function(type, data) { var id = coerceId(data.id); - var record = this.recordForId(type, id); + var internalModel = this._internalModelForId(type, id); - record.setupData(data); - this.recordArrayManager.recordDidChange(record); + internalModel.setupData(data); - return record; + this.recordArrayManager.recordDidChange(internalModel); + + return internalModel; }, /* @@ -1484,7 +1527,11 @@ Store = Service.extend({ configurable: false, get: function() { Ember.deprecate('Usage of `typeKey` has been deprecated and will be removed in Ember Data 1.0. It has been replaced by `modelName` on the model class.'); - return Ember.String.camelize(this.modelName); + var typeKey = this.modelName; + if (typeKey) { + typeKey = Ember.String.camelize(this.modelName); + } + return typeKey; }, set: function() { Ember.assert('Setting typeKey is not supported. In addition, typeKey has also been deprecated in favor of modelName. Setting modelName is also not supported.'); @@ -1569,6 +1616,11 @@ Store = Service.extend({ updated. */ push: function(modelName, data) { + var internalModel = this._pushInternalModel(modelName, data); + return internalModel.getRecord(); + }, + + _pushInternalModel: function(modelName, data) { Ember.assert("Expected an object as `data` in a call to `push` for " + modelName + " , but was " + data, Ember.typeOf(data) === 'object'); Ember.assert("You must include an `id` for " + modelName + " in an object passed to `push`", data.id != null && data.id !== ''); @@ -1589,17 +1641,15 @@ Store = Service.extend({ } // Actually load the record into the store. + var internalModel = this._load(type, data); - this._load(type, data); - - var record = this.recordForId(type, data.id); var store = this; this._backburner.join(function() { - store._backburner.schedule('normalizeRelationships', store, '_setupRelationships', record, type, data); + store._backburner.schedule('normalizeRelationships', store, '_setupRelationships', internalModel, type, data); }); - return record; + return internalModel; }, _setupRelationships: function(record, type, data) { @@ -1758,7 +1808,7 @@ Store = Service.extend({ @param {Object} data @return {DS.Model} record */ - buildRecord: function(type, id, data) { + buildInternalModel: function(type, id, data) { var typeMap = this.typeMapFor(type); var idToRecord = typeMap.idToRecord; @@ -1767,25 +1817,17 @@ Store = Service.extend({ // lookupFactory should really return an object that creates // instances with the injections applied - var record = type._create({ - id: id, - store: this, - container: this.container - }); - - if (data) { - record.setupData(data); - } + var internalModel = new InternalModel(type, id, this, this.container, data); // if we're creating an item, this process will be done // later, once the object has been persisted. if (id) { - idToRecord[id] = record; + idToRecord[id] = internalModel; } - typeMap.records.push(record); + typeMap.records.push(internalModel); - return record; + return internalModel; }, //Called by the state machine to notify the store that the record is ready to be interacted with @@ -1817,7 +1859,7 @@ Store = Service.extend({ @param {DS.Model} record */ _dematerializeRecord: function(record) { - var type = record.constructor; + var type = record.type; var typeMap = this.typeMapFor(type); var id = get(record, 'id'); @@ -1979,20 +2021,27 @@ function normalizeRelationships(store, type, data, record) { } function deserializeRecordId(store, data, key, relationship, id) { - if (isNone(id) || id instanceof Model) { + if (isNone(id)) { return; } + + //If record objects were given to push directly, uncommon, not sure whether we should actually support + if (id instanceof Model) { + data[key] = id._internalModel; + return; + } + Ember.assert("A " + relationship.parentType + " record was pushed into the store with the value of " + key + " being " + Ember.inspect(id) + ", but " + key + " is a belongsTo relationship so the value must not be an array. You should probably check your data payload or serializer.", !Ember.isArray(id)); var type; if (typeof id === 'number' || typeof id === 'string') { type = typeFor(relationship, key, data); - data[key] = store.recordForId(type, id); + data[key] = store._internalModelForId(type, id); } else if (typeof id === 'object') { // hasMany polymorphic Ember.assert('Ember Data expected a number or string to represent the record(s) in the `' + relationship.key + '` relationship instead it found an object. If this is a polymorphic relationship please specify a `type` key. If this is an embedded relationship please include the `DS.EmbeddedRecordsMixin` and specify the `' + relationship.key +'` property in your serializer\'s attrs object.', id.type); - data[key] = store.recordForId(id.type, id.id); + data[key] = store._internalModelForId(id.type, id.id); } } @@ -2025,7 +2074,7 @@ function defaultSerializer(container) { } function _commit(adapter, store, operation, snapshot) { - var record = snapshot.record; + var record = snapshot._internalModel; var type = snapshot.type; var promise = adapter[operation](store, type, snapshot); var serializer = serializerForAdapter(store, adapter, type); @@ -2062,7 +2111,7 @@ function _commit(adapter, store, operation, snapshot) { } function setupRelationships(store, record, data) { - var typeClass = record.constructor; + var typeClass = record.type; typeClass.eachRelationship(function(key, descriptor) { var kind = descriptor.kind; diff --git a/packages/ember-data/lib/system/store/finders.js b/packages/ember-data/lib/system/store/finders.js index 4697be92462..27b5707e3bb 100644 --- a/packages/ember-data/lib/system/store/finders.js +++ b/packages/ember-data/lib/system/store/finders.js @@ -9,8 +9,8 @@ import { } from "ember-data/system/store/serializers"; -var get = Ember.get; var Promise = Ember.RSVP.Promise; +var map = Ember.EnumerableUtils.map; export function _find(adapter, store, typeClass, id, record) { var snapshot = record._createSnapshot(); @@ -26,12 +26,14 @@ export function _find(adapter, store, typeClass, id, record) { return store._adapterRun(function() { var payload = serializer.extract(store, typeClass, adapterPayload, id, 'find'); - return store.push(typeClass, payload); + //TODO Optimize + var record = store.push(typeClass, payload); + return record._internalModel; }); }, function(error) { record.notFound(); - if (get(record, 'isEmpty')) { - store.unloadRecord(record); + if (record.isEmpty()) { + record.unloadRecord(); } throw error; @@ -58,7 +60,9 @@ export function _findMany(adapter, store, typeClass, ids, records) { Ember.assert("The response from a findMany must be an Array, not " + Ember.inspect(payload), Ember.typeOf(payload) === 'array'); - return store.pushMany(typeClass, payload); + //TODO Optimize, no need to materialize here + var records = store.pushMany(typeClass, payload); + return map(records, function(record) { return record._internalModel; }); }); }, null, "DS: Extract payload of " + typeClass); } @@ -79,8 +83,9 @@ export function _findHasMany(adapter, store, record, link, relationship) { Ember.assert("The response from a findHasMany must be an Array, not " + Ember.inspect(payload), Ember.typeOf(payload) === 'array'); + //TODO Use a non record creating push var records = store.pushMany(relationship.type, payload); - return records; + return map(records, function(record) { return record._internalModel; }); }); }, null, "DS: Extract payload of " + record + " : hasMany " + relationship.type); } @@ -104,7 +109,8 @@ export function _findBelongsTo(adapter, store, record, link, relationship) { } var record = store.push(relationship.type, payload); - return record; + //TODO Optimize + return record._internalModel; }); }, null, "DS: Extract payload of " + record + " : " + relationship.type); } diff --git a/packages/ember-data/tests/integration/adapter/rest-adapter-test.js b/packages/ember-data/tests/integration/adapter/rest-adapter-test.js index 7098801ca74..5b2e2218b02 100644 --- a/packages/ember-data/tests/integration/adapter/rest-adapter-test.js +++ b/packages/ember-data/tests/integration/adapter/rest-adapter-test.js @@ -486,7 +486,7 @@ test("create - response can contain relationships the client doesn't yet know ab var postRecords = store.typeMapFor(Post).records; for (var i = 0; i < postRecords.length; i++) { - equal(post, postRecords[i], "The object in the identity map is the same"); + equal(post, postRecords[i].getRecord(), "The object in the identity map is the same"); } })); }); diff --git a/packages/ember-data/tests/integration/adapter/store-adapter-test.js b/packages/ember-data/tests/integration/adapter/store-adapter-test.js index 50e854e3eb1..605b13c9f1a 100644 --- a/packages/ember-data/tests/integration/adapter/store-adapter-test.js +++ b/packages/ember-data/tests/integration/adapter/store-adapter-test.js @@ -664,7 +664,7 @@ test("if a updated record is marked as erred by the server, it enters an error s }; var person = run(function() { - return store.push(Person, { id: 1, name: "John Doe" }); + return store.push('person', { id: 1, name: "John Doe" }); }); run(store, 'find', 'person', 1).then(async(function(record) { diff --git a/packages/ember-data/tests/integration/lifecycle-hooks-test.js b/packages/ember-data/tests/integration/lifecycle-hooks-test.js index dab3da4c7a6..3625e35c9e0 100644 --- a/packages/ember-data/tests/integration/lifecycle-hooks-test.js +++ b/packages/ember-data/tests/integration/lifecycle-hooks-test.js @@ -28,7 +28,7 @@ asyncTest("When the adapter acknowledges that a record has been created, a `didC var person; run(function() { - person = env.store.createRecord(Person, { name: "Yehuda Katz" }); + person = env.store.createRecord('person', { name: "Yehuda Katz" }); }); person.on('didCreate', function() { @@ -50,7 +50,7 @@ test("When the adapter acknowledges that a record has been created without a new var person; run(function() { - person = env.store.createRecord(Person, { id: 99, name: "Yehuda Katz" }); + person = env.store.createRecord('person', { id: 99, name: "Yehuda Katz" }); }); person.on('didCreate', function() { diff --git a/packages/ember-data/tests/integration/record-array-manager-test.js b/packages/ember-data/tests/integration/record-array-manager-test.js index fbef834d1b8..f059fff3787 100644 --- a/packages/ember-data/tests/integration/record-array-manager-test.js +++ b/packages/ember-data/tests/integration/record-array-manager-test.js @@ -84,17 +84,17 @@ test("destroying the store correctly cleans everything up", function() { equal(filterd2Summary.called.length, 0); - equal(person._recordArrays.list.length, 2, 'expected the person to be a member of 2 recordArrays'); + equal(person._internalModel._recordArrays.list.length, 2, 'expected the person to be a member of 2 recordArrays'); Ember.run(filterd2, filterd2.destroy); - equal(person._recordArrays.list.length, 1, 'expected the person to be a member of 1 recordArrays'); + equal(person._internalModel._recordArrays.list.length, 1, 'expected the person to be a member of 1 recordArrays'); equal(filterd2Summary.called.length, 1); Ember.run(manager, manager.destroy); - equal(person._recordArrays.list.length, 0, 'expected the person to be a member of no recordArrays'); + equal(person._internalModel._recordArrays.list.length, 0, 'expected the person to be a member of no recordArrays'); equal(filterd2Summary.called.length, 1); diff --git a/packages/ember-data/tests/integration/records/save-test.js b/packages/ember-data/tests/integration/records/save-test.js index cc432f3169d..10fc17fbd50 100644 --- a/packages/ember-data/tests/integration/records/save-test.js +++ b/packages/ember-data/tests/integration/records/save-test.js @@ -18,19 +18,28 @@ module("integration/records/save - Save Record", { }); test("Will resolve save on success", function() { - expect(1); + expect(4); var post; run(function() { post = env.store.createRecord('post', { title: 'toto' }); }); + var deferred = Ember.RSVP.defer(); env.adapter.createRecord = function(store, type, snapshot) { - return Ember.RSVP.resolve({ id: 123 }); + return deferred.promise; }; run(function() { - post.save().then(function() { + var saved = post.save(); + + // `save` returns a PromiseObject which allows to call get on it + equal(saved.get('id'), undefined); + + deferred.resolve({ id: 123 }); + saved.then(function(model) { ok(true, 'save operation was resolved'); + equal(saved.get('id'), 123); + equal(model, post, "resolves with the model"); }); }); }); diff --git a/packages/ember-data/tests/integration/relationships/belongs-to-test.js b/packages/ember-data/tests/integration/relationships/belongs-to-test.js index a906968a355..8a38c9006a9 100644 --- a/packages/ember-data/tests/integration/relationships/belongs-to-test.js +++ b/packages/ember-data/tests/integration/relationships/belongs-to-test.js @@ -602,7 +602,7 @@ test("belongsTo hasData async loaded", function () { run(function() { store.find('book', 1).then(function(book) { - var relationship = book._relationships['author']; + var relationship = book._internalModel._relationships['author']; equal(relationship.hasData, true, 'relationship has data'); }); }); @@ -617,7 +617,7 @@ test("belongsTo hasData sync loaded", function () { run(function() { store.find('book', 1).then(function(book) { - var relationship = book._relationships['author']; + var relationship = book._internalModel._relationships['author']; equal(relationship.hasData, true, 'relationship has data'); }); }); @@ -636,7 +636,7 @@ test("belongsTo hasData async not loaded", function () { run(function() { store.find('book', 1).then(function(book) { - var relationship = book._relationships['author']; + var relationship = book._internalModel._relationships['author']; equal(relationship.hasData, false, 'relationship does not have data'); }); }); @@ -651,7 +651,7 @@ test("belongsTo hasData sync not loaded", function () { run(function() { store.find('book', 1).then(function(book) { - var relationship = book._relationships['author']; + var relationship = book._internalModel._relationships['author']; equal(relationship.hasData, false, 'relationship does not have data'); }); }); @@ -666,7 +666,7 @@ test("belongsTo hasData async created", function () { run(function() { var book = store.createRecord('book', { name: 'The Greatest Book' }); - var relationship = book._relationships['author']; + var relationship = book._internalModel._relationships['author']; equal(relationship.hasData, true, 'relationship has data'); }); }); @@ -676,7 +676,7 @@ test("belongsTo hasData sync created", function () { run(function() { var book = store.createRecord('book', { name: 'The Greatest Book' }); - var relationship = book._relationships['author']; + var relationship = book._internalModel._relationships['author']; equal(relationship.hasData, true, 'relationship has data'); }); }); diff --git a/packages/ember-data/tests/integration/relationships/has-many-test.js b/packages/ember-data/tests/integration/relationships/has-many-test.js index ad4981cfa39..fc029cb7628 100644 --- a/packages/ember-data/tests/integration/relationships/has-many-test.js +++ b/packages/ember-data/tests/integration/relationships/has-many-test.js @@ -127,7 +127,7 @@ test("adapter.findMany only gets unique IDs even if duplicate IDs are present in }); // This tests the case where a serializer materializes a has-many -// relationship as a reference that it can fetch lazily. The most +// relationship as a internalModel that it can fetch lazily. The most // common use case of this is to provide a URL to a collection that // is loaded later. test("A serializer can materialize a hasMany as an opaque token that can be lazily fetched via the adapter's findHasMany hook", function() { @@ -970,7 +970,7 @@ test("dual non-async HM <-> BT", function() { deepEqual(post, commentPost, 'expect the new comments post, to be the correct post'); ok(postComments, "comments should exist"); - equal(postCommentsLength, 2, "comment's post should have a reference back to comment"); + equal(postCommentsLength, 2, "comment's post should have a internalModel back to comment"); ok(postComments && postComments.indexOf(firstComment) !== -1, 'expect to contain first comment'); ok(postComments && postComments.indexOf(comment) !== -1, 'expected to contain the new comment'); }); @@ -1218,7 +1218,7 @@ test("Relationship.clear removes all records correctly", function() { }); run(function() { - post._relationships['comments'].clear(); + post._internalModel._relationships['comments'].clear(); var comments = Ember.A(env.store.all('comment')); deepEqual(comments.mapBy('post'), [null, null, null]); }); @@ -1350,7 +1350,7 @@ test("hasMany hasData async loaded", function () { run(function() { store.find('chapter', 1).then(function(chapter) { - var relationship = chapter._relationships['pages']; + var relationship = chapter._internalModel._relationships['pages']; equal(relationship.hasData, true, 'relationship has data'); }); }); @@ -1365,7 +1365,7 @@ test("hasMany hasData sync loaded", function () { run(function() { store.find('chapter', 1).then(function(chapter) { - var relationship = chapter._relationships['pages']; + var relationship = chapter._internalModel._relationships['pages']; equal(relationship.hasData, true, 'relationship has data'); }); }); @@ -1384,7 +1384,7 @@ test("hasMany hasData async not loaded", function () { run(function() { store.find('chapter', 1).then(function(chapter) { - var relationship = chapter._relationships['pages']; + var relationship = chapter._internalModel._relationships['pages']; equal(relationship.hasData, false, 'relationship does not have data'); }); }); @@ -1399,7 +1399,7 @@ test("hasMany hasData sync not loaded", function () { run(function() { store.find('chapter', 1).then(function(chapter) { - var relationship = chapter._relationships['pages']; + var relationship = chapter._internalModel._relationships['pages']; equal(relationship.hasData, false, 'relationship does not have data'); }); }); @@ -1414,7 +1414,7 @@ test("hasMany hasData async created", function () { run(function() { var chapter = store.createRecord('chapter', { title: 'The Story Begins' }); - var relationship = chapter._relationships['pages']; + var relationship = chapter._internalModel._relationships['pages']; equal(relationship.hasData, true, 'relationship has data'); }); }); @@ -1424,7 +1424,7 @@ test("hasMany hasData sync created", function () { run(function() { var chapter = store.createRecord('chapter', { title: 'The Story Begins' }); - var relationship = chapter._relationships['pages']; + var relationship = chapter._internalModel._relationships['pages']; equal(relationship.hasData, true, 'relationship has data'); }); }); diff --git a/packages/ember-data/tests/unit/model-test.js b/packages/ember-data/tests/unit/model-test.js index 38818481bd2..c5a9866104d 100644 --- a/packages/ember-data/tests/unit/model-test.js +++ b/packages/ember-data/tests/unit/model-test.js @@ -140,17 +140,19 @@ test("a collision of a record's id with object function's name", function() { } }); -test("it should use `_reference` and not `reference` to store its reference", function() { +/* +test("it should use `_internalModel` and not `internalModel` to store its internalModel", function() { expect(1); run(function() { store.push(Person, { id: 1 }); store.find(Person, 1).then(function(record) { - equal(record.get('reference'), undefined, "doesn't shadow reference key"); + equal(record.get('_internalModel'), undefined, "doesn't shadow internalModel key"); }); }); }); +*/ test("it should cache attributes", function() { expect(2); @@ -407,15 +409,15 @@ test("setting a property back to its original value removes the property from th run(function() { store.find(Person, 1).then(function(person) { - equal(person._attributes.name, undefined, "the `_attributes` hash is clean"); + equal(person._internalModel._attributes.name, undefined, "the `_attributes` hash is clean"); set(person, 'name', "Niceguy Dale"); - equal(person._attributes.name, "Niceguy Dale", "the `_attributes` hash contains the changed value"); + equal(person._internalModel._attributes.name, "Niceguy Dale", "the `_attributes` hash contains the changed value"); set(person, 'name', "Scumbag Dale"); - equal(person._attributes.name, undefined, "the `_attributes` hash is reset"); + equal(person._internalModel._attributes.name, undefined, "the `_attributes` hash is reset"); }); }); }); diff --git a/packages/ember-data/tests/unit/model/lifecycle-callbacks-test.js b/packages/ember-data/tests/unit/model/lifecycle-callbacks-test.js index 4855eb1f245..b7598ee3a79 100644 --- a/packages/ember-data/tests/unit/model/lifecycle-callbacks-test.js +++ b/packages/ember-data/tests/unit/model/lifecycle-callbacks-test.js @@ -32,6 +32,32 @@ test("a record receives a didLoad callback when it has finished loading", functi }); }); +test("TEMPORARY: a record receives a didLoad callback once it materializes if it wasn't materialized when loaded", function() { + expect(2); + var didLoadCalled = 0; + var Person = DS.Model.extend({ + name: DS.attr(), + didLoad: function() { + didLoadCalled++; + } + }); + + var store = createStore({ + person: Person + }); + + run(function() { + store._pushInternalModel('person', { id: 1 }); + equal(didLoadCalled, 0, "didLoad was not called"); + }); + run(function() { + store.getById('person', 1); + }); + run(function() { + equal(didLoadCalled, 1, "didLoad was called"); + }); +}); + test("a record receives a didUpdate callback when it has finished updating", function() { expect(5); diff --git a/packages/ember-data/tests/unit/model/relationships/record-array-test.js b/packages/ember-data/tests/unit/model/relationships/record-array-test.js index 76809505d26..be273c2a158 100644 --- a/packages/ember-data/tests/unit/model/relationships/record-array-test.js +++ b/packages/ember-data/tests/unit/model/relationships/record-array-test.js @@ -11,18 +11,19 @@ test("updating the content of a RecordArray updates its content", function() { var env = setupStore({ tag: Tag }); var store = env.store; - var records, tags; + var records, tags, internalModel; run(function() { records = store.pushMany('tag', [{ id: 5, name: "friendly" }, { id: 2, name: "smarmy" }, { id: 12, name: "oohlala" }]); - tags = DS.RecordArray.create({ content: Ember.A(records.slice(0, 2)), store: store, type: Tag }); + internalModel = Ember.A(records).mapBy('_internalModel'); + tags = DS.RecordArray.create({ content: Ember.A(internalModel.slice(0, 2)), store: store, type: Tag }); }); var tag = tags.objectAt(0); equal(get(tag, 'name'), "friendly", "precond - we're working with the right tags"); run(function() { - set(tags, 'content', Ember.A(records.slice(1, 3))); + set(tags, 'content', Ember.A(internalModel.slice(1, 3))); }); tag = tags.objectAt(0); diff --git a/packages/ember-data/tests/unit/store/unload-test.js b/packages/ember-data/tests/unit/store/unload-test.js index 5816b16943c..8791b87b12b 100644 --- a/packages/ember-data/tests/unit/store/unload-test.js +++ b/packages/ember-data/tests/unit/store/unload-test.js @@ -37,7 +37,7 @@ test("unload a dirty record", function() { store.find(Record, 1).then(function(record) { record.set('title', 'toto2'); - record.send('willCommit'); + record._internalModel.send('willCommit'); equal(get(record, 'isDirty'), true, "record is dirty"); @@ -47,7 +47,7 @@ test("unload a dirty record", function() { // force back into safe to unload mode. run(function() { - record.transitionTo('deleted.saved'); + record._internalModel.transitionTo('deleted.saved'); }); }); });