From 31583dbac284db55102f5a6dfb08af5e18a2971b Mon Sep 17 00:00:00 2001 From: James Murphy Date: Sat, 14 Feb 2015 22:42:42 +1100 Subject: [PATCH] Adapters and Serializers are Store Managed Adapters and Serializers are now store managed - with each store instance holding a unique instance of each serializer and adapter. They are no longer singletons. This allows multiple stores to be used with ember-data. --- .../lib/initializers/store-injections.js | 1 - packages/ember-data/lib/initializers/store.js | 3 + packages/ember-data/lib/system/store.js | 92 ++++++++++-- .../ember-data/lib/system/store/finders.js | 12 +- .../lib/system/store/serializers.js | 15 +- .../tests/integration/multiple_stores_test.js | 134 ++++++++++++++++++ .../relationships/belongs-to-test.js | 8 +- .../tests/integration/setup-container-test.js | 20 ++- tests/ember-configuration.js | 3 + 9 files changed, 251 insertions(+), 37 deletions(-) create mode 100644 packages/ember-data/tests/integration/multiple_stores_test.js diff --git a/packages/ember-data/lib/initializers/store-injections.js b/packages/ember-data/lib/initializers/store-injections.js index b41436a6a4..16dca7f409 100644 --- a/packages/ember-data/lib/initializers/store-injections.js +++ b/packages/ember-data/lib/initializers/store-injections.js @@ -8,6 +8,5 @@ export default function initializeStoreInjections(registry) { registry.injection('controller', 'store', 'store:main'); registry.injection('route', 'store', 'store:main'); - registry.injection('serializer', 'store', 'store:main'); registry.injection('data-adapter', 'store', 'store:main'); } diff --git a/packages/ember-data/lib/initializers/store.js b/packages/ember-data/lib/initializers/store.js index 75697b2a24..b7470688b2 100644 --- a/packages/ember-data/lib/initializers/store.js +++ b/packages/ember-data/lib/initializers/store.js @@ -15,6 +15,9 @@ export default function initializeStore(registry, application) { Ember.deprecate('Specifying a custom Store for Ember Data on your global namespace as `App.Store` ' + 'has been deprecated. Please use `App.ApplicationStore` instead.', !(application && application.Store)); + registry.optionsForType('serializer', { singleton: false }); + registry.optionsForType('adapter', { singleton: false }); + registry.register('store:main', registry.lookupFactory('store:application') || (application && application.Store) || Store); // allow older names to be looked up diff --git a/packages/ember-data/lib/system/store.js b/packages/ember-data/lib/system/store.js index 703b5c7bd2..dc9e010b2e 100644 --- a/packages/ember-data/lib/system/store.js +++ b/packages/ember-data/lib/system/store.js @@ -26,7 +26,6 @@ import { } from "ember-data/system/store/common"; import { - serializerFor, serializerForAdapter } from "ember-data/system/store/serializers"; @@ -210,6 +209,7 @@ Store = Service.extend({ store: this }); this._pendingSave = []; + this._containerCache = Ember.create(null); //Used to keep track of all the find requests that need to be coalesced this._pendingFetch = Map.create(); }, @@ -276,7 +276,8 @@ Store = Service.extend({ if (DS.Adapter.detect(adapter)) { adapter = adapter.create({ - container: this.container + container: this.container, + store: this }); } @@ -1785,20 +1786,26 @@ Store = Service.extend({ // ...................... /** - Returns the adapter for a given type. + Returns an instance of the adapter for a given type. For + example, `adapterFor('person')` will return an instance of + `App.PersonAdapter`. + + If no `App.PersonAdapter` is found, this method will look + for an `App.ApplicationAdapter` (the default adapter for + your entire application). + + If no `App.ApplicationAdapter` is found, it will return + the value of the `defaultAdapter`. @method adapterFor @private - @param {subclass of DS.Model} type + @param {String or subclass of DS.Model} type @return DS.Adapter */ adapterFor: function(type) { - var adapter; - var container = this.container; + type = this.modelFor(type); - if (container) { - adapter = container.lookup('adapter:' + type.typeKey) || container.lookup('adapter:application'); - } + var adapter = this.lookupAdapter(type.typeKey) || this.lookupAdapter('application'); return adapter || get(this, 'defaultAdapter'); }, @@ -1820,19 +1827,70 @@ Store = Service.extend({ for an `App.ApplicationSerializer` (the default serializer for your entire application). - If no `App.ApplicationSerializer` is found, it will fall back + if no `App.ApplicationSerializer` is found, it will attempt + to get the `defaultSerializer` from the `PersonAdapter` + (`adapterFor('person')`). + + If a serializer cannot be found on the adapter, it will fall back to an instance of `DS.JSONSerializer`. @method serializerFor @private - @param {String} type the record to serialize + @param {String or subclass of DS.Model} type the record to serialize @return {DS.Serializer} */ serializerFor: function(type) { type = this.modelFor(type); - var adapter = this.adapterFor(type); - return serializerFor(this.container, type.typeKey, adapter && adapter.defaultSerializer); + var serializer = this.lookupSerializer(type.typeKey) || this.lookupSerializer('application'); + + if (!serializer) { + var adapter = this.adapterFor(type); + serializer = this.lookupSerializer(get(adapter, 'defaultSerializer')); + } + + if (!serializer) { + serializer = this.lookupSerializer('-default'); + } + + return serializer; + }, + + /** + Retrieve a particular instance from the + container cache. If not found, creates it and + placing it in the cache. + + Enabled a store to manage local instances of + adapters and serializers. + + @method retrieveManagedInstance + @private + @param {String} type the object type + @param {String} type the object name + @return {Ember.Object} + */ + retrieveManagedInstance: function(type, name) { + var key = type+":"+name; + + if (!this._containerCache[key]) { + var instance = this.container.lookup(key); + + if (instance) { + set(instance, 'store', this); + this._containerCache[key] = instance; + } + } + + return this._containerCache[key]; + }, + + lookupAdapter: function(name) { + return this.retrieveManagedInstance('adapter', name); + }, + + lookupSerializer: function(name) { + return this.retrieveManagedInstance('serializer', name); }, willDestroy: function() { @@ -1849,6 +1907,12 @@ Store = Service.extend({ return typeMaps[entry]['type']; } + for (var cacheKey in this._containerCache) { + this._containerCache[cacheKey].destroy(); + delete this._containerCache[cacheKey]; + } + + delete this._containerCache; }, /** @@ -1929,7 +1993,7 @@ function defaultSerializer(container) { function _commit(adapter, store, operation, record) { var type = record.constructor; var promise = adapter[operation](store, type, record); - var serializer = serializerForAdapter(adapter, type); + var serializer = serializerForAdapter(store, adapter, type); var label = "DS: Extract and notify about " + operation + " completion of " + record; Ember.assert("Your adapter's '" + operation + "' method must return a value, but it returned `undefined", promise !==undefined); diff --git a/packages/ember-data/lib/system/store/finders.js b/packages/ember-data/lib/system/store/finders.js index c2906c165a..66405d5980 100644 --- a/packages/ember-data/lib/system/store/finders.js +++ b/packages/ember-data/lib/system/store/finders.js @@ -14,7 +14,7 @@ var Promise = Ember.RSVP.Promise; export function _find(adapter, store, type, id, record) { var promise = adapter.find(store, type, id, record); - var serializer = serializerForAdapter(adapter, type); + var serializer = serializerForAdapter(store, adapter, type); var label = "DS: Handle Adapter#find of " + type + " with id: " + id; promise = Promise.cast(promise, label); @@ -42,7 +42,7 @@ export function _find(adapter, store, type, id, record) { export function _findMany(adapter, store, type, ids, records) { var promise = adapter.findMany(store, type, ids, records); - var serializer = serializerForAdapter(adapter, type); + var serializer = serializerForAdapter(store, adapter, type); var label = "DS: Handle Adapter#findMany of " + type; if (promise === undefined) { @@ -65,7 +65,7 @@ export function _findMany(adapter, store, type, ids, records) { export function _findHasMany(adapter, store, record, link, relationship) { var promise = adapter.findHasMany(store, record, link, relationship); - var serializer = serializerForAdapter(adapter, relationship.type); + var serializer = serializerForAdapter(store, adapter, relationship.type); var label = "DS: Handle Adapter#findHasMany of " + record + " : " + relationship.type; promise = Promise.cast(promise, label); @@ -86,7 +86,7 @@ export function _findHasMany(adapter, store, record, link, relationship) { export function _findBelongsTo(adapter, store, record, link, relationship) { var promise = adapter.findBelongsTo(store, record, link, relationship); - var serializer = serializerForAdapter(adapter, relationship.type); + var serializer = serializerForAdapter(store, adapter, relationship.type); var label = "DS: Handle Adapter#findBelongsTo of " + record + " : " + relationship.type; promise = Promise.cast(promise, label); @@ -109,7 +109,7 @@ export function _findBelongsTo(adapter, store, record, link, relationship) { export function _findAll(adapter, store, type, sinceToken) { var promise = adapter.findAll(store, type, sinceToken); - var serializer = serializerForAdapter(adapter, type); + var serializer = serializerForAdapter(store, adapter, type); var label = "DS: Handle Adapter#findAll of " + type; promise = Promise.cast(promise, label); @@ -131,7 +131,7 @@ export function _findAll(adapter, store, type, sinceToken) { export function _findQuery(adapter, store, type, query, recordArray) { var promise = adapter.findQuery(store, type, query, recordArray); - var serializer = serializerForAdapter(adapter, type); + var serializer = serializerForAdapter(store, adapter, type); var label = "DS: Handle Adapter#findQuery of " + type; promise = Promise.cast(promise, label); diff --git a/packages/ember-data/lib/system/store/serializers.js b/packages/ember-data/lib/system/store/serializers.js index 077b5acf2e..58f4a976a4 100644 --- a/packages/ember-data/lib/system/store/serializers.js +++ b/packages/ember-data/lib/system/store/serializers.js @@ -1,17 +1,8 @@ -export function serializerFor(container, type, defaultSerializer) { - return container.lookup('serializer:'+type) || - container.lookup('serializer:application') || - container.lookup('serializer:' + defaultSerializer) || - container.lookup('serializer:-default'); -} - -export function serializerForAdapter(adapter, type) { +export function serializerForAdapter(store, adapter, type) { var serializer = adapter.serializer; - var defaultSerializer = adapter.defaultSerializer; - var container = adapter.container; - if (container && serializer === undefined) { - serializer = serializerFor(container, type.typeKey, defaultSerializer); + if (serializer === undefined) { + serializer = store.serializerFor(type); } if (serializer === null || serializer === undefined) { diff --git a/packages/ember-data/tests/integration/multiple_stores_test.js b/packages/ember-data/tests/integration/multiple_stores_test.js new file mode 100644 index 0000000000..c1ea91daf1 --- /dev/null +++ b/packages/ember-data/tests/integration/multiple_stores_test.js @@ -0,0 +1,134 @@ +var env; +var SuperVillain, HomePlanet, EvilMinion; +var run = Ember.run; + +module("integration/multiple_stores - Multiple Stores Tests", { + setup: function() { + SuperVillain = DS.Model.extend({ + firstName: DS.attr('string'), + lastName: DS.attr('string'), + homePlanet: DS.belongsTo("homePlanet", { inverse: 'villains' }), + evilMinions: DS.hasMany("evilMinion") + }); + HomePlanet = DS.Model.extend({ + name: DS.attr('string'), + villains: DS.hasMany('superVillain', { inverse: 'homePlanet' }) + }); + EvilMinion = DS.Model.extend({ + superVillain: DS.belongsTo('superVillain'), + name: DS.attr('string') + }); + + env = setupStore({ + superVillain: SuperVillain, + homePlanet: HomePlanet, + evilMinion: EvilMinion + }); + + env.registry.register('serializer:application', DS.ActiveModelSerializer); + env.registry.register('serializer:-active-model', DS.ActiveModelSerializer); + env.registry.register('adapter:-active-model', DS.ActiveModelAdapter); + + env.registry.register('store:store-a', DS.Store); + env.registry.register('store:store-b', DS.Store); + + env.store_a = env.container.lookup('store:store-a'); + env.store_b = env.container.lookup('store:store-b'); + }, + + teardown: function() { + run(env.store, 'destroy'); + } +}); + +test("should be able to push into multiple stores", function() { + env.registry.register('adapter:homePlanet', DS.ActiveModelAdapter); + env.registry.register('serializer:homePlanet', DS.ActiveModelSerializer); + + var home_planet_main = { id: '1', name: 'Earth' }; + var home_planet_a = { id: '1', name: 'Mars' }; + var home_planet_b = { id: '1', name: 'Saturn' }; + + run(env.store, 'push', 'homePlanet', home_planet_main); + run(env.store_a, 'push', 'homePlanet', home_planet_a); + run(env.store_b, 'push', 'homePlanet', home_planet_b); + + run(env.store, 'find', 'homePlanet', 1).then(async(function(homePlanet) { + equal(homePlanet.get('name'), "Earth"); + })); + + run(env.store_a, 'find', 'homePlanet', 1).then(async(function(homePlanet) { + equal(homePlanet.get('name'), "Mars"); + })); + + run(env.store_b, 'find', 'homePlanet', 1).then(async(function(homePlanet) { + equal(homePlanet.get('name'), "Saturn"); + })); + +}); + +test("embedded records should be created in multiple stores", function() { + env.registry.register('adapter:superVillain', DS.ActiveModelAdapter); + env.registry.register('adapter:homePlanet', DS.ActiveModelAdapter); + + env.registry.register('serializer:homePlanet', DS.ActiveModelSerializer.extend(DS.EmbeddedRecordsMixin, { + attrs: { + villains: { embedded: 'always' } + } + })); + + var serializer_main = env.store.serializerFor("homePlanet"); + var serializer_a = env.store_a.serializerFor("homePlanet"); + var serializer_b = env.store_b.serializerFor("homePlanet"); + + var json_hash_main = { + home_planet: { + id: "1", + name: "Earth", + villains: [{ + id: "1", + first_name: "Tom", + last_name: "Dale" + }] + } + }; + var json_hash_a = { + home_planet: { + id: "1", + name: "Mars", + villains: [{ + id: "1", + first_name: "James", + last_name: "Murphy" + }] + } + }; + var json_hash_b = { + home_planet: { + id: "1", + name: "Saturn", + villains: [{ + id: "1", + first_name: "Jade", + last_name: "John" + }] + } + }; + var json_main, json_a, json_b; + + run(function() { + json_main = serializer_main.extractSingle(env.store, HomePlanet, json_hash_main); + equal(env.store.hasRecordForId("superVillain", "1"), true, "superVillain should exist in store:main"); + }); + + run(function() { + json_a = serializer_a.extractSingle(env.store_a, HomePlanet, json_hash_a); + equal(env.store_a.hasRecordForId("superVillain", "1"), true, "superVillain should exist in store:store-a"); + }); + + run(function() { + json_b = serializer_b.extractSingle(env.store_b, HomePlanet, json_hash_b); + equal(env.store_b.hasRecordForId("superVillain", "1"), true, "superVillain should exist in store:store-b"); + }); + +}); 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 d7838613b2..c75c613007 100644 --- a/packages/ember-data/tests/integration/relationships/belongs-to-test.js +++ b/packages/ember-data/tests/integration/relationships/belongs-to-test.js @@ -63,6 +63,10 @@ module("integration/relationship/belongs_to Belongs-To Relationships", { author: Author }); + + env.registry.optionsForType('serializer', { singleton: false }); + env.registry.optionsForType('adapter', { singleton: false }); + env.registry.register('serializer:user', DS.JSONSerializer.extend({ attrs: { favouriteMessage: { embedded: 'always' } @@ -176,7 +180,9 @@ test("The store can load a polymorphic belongsTo association", function() { }); test("The store can serialize a polymorphic belongsTo association", function() { - env.serializer.serializePolymorphicType = function(record, json, relationship) { + var serializerInstance = store.serializerFor('comment'); + + serializerInstance.serializePolymorphicType = function(record, json, relationship) { ok(true, "The serializer's serializePolymorphicType method should be called"); json["message_type"] = "post"; }; diff --git a/packages/ember-data/tests/integration/setup-container-test.js b/packages/ember-data/tests/integration/setup-container-test.js index 5d2ef1db14..724f6537bc 100644 --- a/packages/ember-data/tests/integration/setup-container-test.js +++ b/packages/ember-data/tests/integration/setup-container-test.js @@ -50,7 +50,7 @@ test("the deprecated serializer:_default is resolved as serializer:default", fun deprecated = container.lookup('serializer:_default'); }); - ok(deprecated === valid, "they should resolve to the same thing"); + ok(deprecated.constructor === valid.constructor, "they should resolve to the same thing"); }); test("the deprecated serializer:_rest is resolved as serializer:rest", function() { @@ -60,7 +60,7 @@ test("the deprecated serializer:_rest is resolved as serializer:rest", function( deprecated = container.lookup('serializer:_rest'); }); - ok(deprecated === valid, "they should resolve to the same thing"); + ok(deprecated.constructor === valid.constructor, "they should resolve to the same thing"); }); test("the deprecated adapter:_rest is resolved as adapter:rest", function() { @@ -70,7 +70,7 @@ test("the deprecated adapter:_rest is resolved as adapter:rest", function() { deprecated = container.lookup('adapter:_rest'); }); - ok(deprecated === valid, "they should resolve to the same thing"); + ok(deprecated.constructor === valid.constructor, "they should resolve to the same thing"); }); test("a deprecation is made when looking up adapter:_rest", function() { @@ -78,3 +78,17 @@ test("a deprecation is made when looking up adapter:_rest", function() { container.lookup('serializer:_default'); }, "You tried to look up 'serializer:_default', but this has been deprecated in favor of 'serializer:-default'."); }); + +test("serializers are not returned as singletons - each lookup should return a different instance", function() { + var serializer1, serializer2; + serializer1 = container.lookup('serializer:-rest'); + serializer2 = container.lookup('serializer:-rest'); + notEqual(serializer1, serializer2); +}); + +test("adapters are not returned as singletons - each lookup should return a different instance", function() { + var adapter1, adapter2; + adapter1 = container.lookup('adapter:-rest'); + adapter2 = container.lookup('adapter:-rest'); + notEqual(adapter1, adapter2); +}); diff --git a/tests/ember-configuration.js b/tests/ember-configuration.js index 2235b8c248..d7f05d6c1d 100644 --- a/tests/ember-configuration.js +++ b/tests/ember-configuration.js @@ -79,6 +79,9 @@ adapter: adapter })); + registry.optionsForType('serializer', { singleton: false }); + registry.optionsForType('adapter', { singleton: false }); + registry.register('serializer:-default', DS.JSONSerializer); registry.register('serializer:-rest', DS.RESTSerializer); registry.register('adapter:-rest', DS.RESTAdapter);