From b7f144c3fbecc674a9775dc89d21f8c7efc65067 Mon Sep 17 00:00:00 2001 From: bjarkehs Date: Thu, 13 Feb 2014 16:05:05 +0100 Subject: [PATCH 1/2] Added loadRelationshipsForMany which adds relationships for arrays of objects. Useful when querying for several objects. Also updated the serializer in order to utilize this --- localstorage_adapter.js | 115 ++++++++++++++++++++++++++++++++++++++++ test/helpers.js | 18 +++++++ test/tests.js | 51 ++++++++++++++++++ 3 files changed, 184 insertions(+) diff --git a/localstorage_adapter.js b/localstorage_adapter.js index 9f37eb3..641e702 100644 --- a/localstorage_adapter.js +++ b/localstorage_adapter.js @@ -16,6 +16,41 @@ DS.LSSerializer = DS.JSONSerializer.extend({ } }, + /** + * Extracts whatever was returned from the adapter. + * + * If the adapter returns relationships in an embedded way, such as follows: + * + * ```js + * { + * "id": 1, + * "title": "Rails Rambo", + * + * "_embedded": { + * "comment": [{ + * "id": 1, + * "comment_title": "FIRST" + * }, { + * "id": 2, + * "comment_title": "Rails is unagi" + * }] + * } + * } + * + * this method will create separated JSON for each resource and then push + * them individually to the Store. + * + * In the end, only the main resource will remain, containing the ids of its + * relationships. Given the relations are already in the Store, we will + * return a JSON with the main resource alone. The Store will sort out the + * associations by itself. + * + * @method extractSingle + * @private + * @param {DS.Store} store the returned store + * @param {DS.Model} type the type/model + * @param {Object} payload returned JSON + */ extractSingle: function(store, type, payload) { if (payload && payload._embedded) { for (var relation in payload._embedded) { @@ -35,6 +70,24 @@ DS.LSSerializer = DS.JSONSerializer.extend({ } return this.normalize(type, payload); + }, + + /** + * This is exactly the same as extractSingle, but used in an array. + * + * @method extractSingle + * @private + * @param {DS.Store} store the returned store + * @param {DS.Model} type the type/model + * @param {Array} payload returned JSONs + */ + extractArray: function(store, type, payload) { + var serializer = this; + + return payload.map(function(record) { + var extracted = serializer.extractSingle(store, type, record); + return serializer.normalize(type, record); + }); } }); @@ -96,6 +149,12 @@ DS.LSAdapter = DS.Adapter.extend(Ember.Evented, { } resolve(results); + }).then(function(records) { + if (records.get('length')) { + return adapter.loadRelationshipsForMany(type, records); + } else { + return records; + } }); }, @@ -117,6 +176,10 @@ DS.LSAdapter = DS.Adapter.extend(Ember.Evented, { var namespace = this._namespaceForType(type), results = this.query(namespace.records, query); + if (results.get('length')) { + results = this.loadRelationshipsForMany(type, results); + } + return Ember.RSVP.resolve(results); }, @@ -392,6 +455,58 @@ DS.LSAdapter = DS.Adapter.extend(Ember.Evented, { return Object.prototype.toString.call(value) === '[object Array]'; }, + /** + * Same as `loadRelationships`, but for an array of records. + * + * @method loadRelationshipsForMany + * @private + * @param {DS.Model} type + * @param {Object} recordsArray + */ + loadRelationshipsForMany: function(type, recordsArray) { + var adapter = this; + + return new Ember.RSVP.Promise(function(resolve, reject) { + var recordsWithRelationships = [], + recordsToBeLoaded = [], + promises = []; + + /** + * Some times Ember puts some stuff in arrays. We want to clean it so + * we know exactly what to iterate over. + */ + for (var i in recordsArray) { + if (recordsArray.hasOwnProperty(i)) { + recordsToBeLoaded.push(recordsArray[i]); + } + } + + var loadNextRecord = function(record) { + /** + * Removes the first item from recordsToBeLoaded + */ + recordsToBeLoaded = recordsToBeLoaded.slice(1); + + var promise = adapter.loadRelationships(type, record); + + promise.then(function(recordWithRelationships) { + recordsWithRelationships.push(recordWithRelationships); + + if (recordsToBeLoaded[0]) { + loadNextRecord(recordsToBeLoaded[0]); + } else { + resolve(recordsWithRelationships); + } + }); + } + + /** + * We start by the first record + */ + loadNextRecord(recordsToBeLoaded[0]); + }); + }, + /** * diff --git a/test/helpers.js b/test/helpers.js index 67a0644..525439e 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -14,6 +14,24 @@ var FIXTURES = { 'i1': { id: 'i1', name: 'one', list: 'l1' }, 'i2': { id: 'i2', name: 'two', list: 'l1' } } + }, + + 'App.Order': { + records: { + 'o1': { id: 'o1', name: 'one', b: true, hours: ['h1', 'h2'] }, + 'o2': { id: 'o2', name: 'two', b: false, hours: [] }, + 'o3': { id: 'o3', name: 'three', b: true, hours: ['h3', 'h4'] }, + 'o4': { id: 'o4', name: 'four', b: true, hours: [] } + } + }, + + 'App.Hour': { + records: { + 'h1': { id: 'h1', name: 'one', amount: 4, order: 'o1' }, + 'h2': { id: 'h2', name: 'two', amount: 3, order: 'o1' }, + 'h3': { id: 'h3', name: 'three', amount: 2, order: 'o3' }, + 'h4': { id: 'h4', name: 'four', amount: 1, order: 'o3' } + } } }; diff --git a/test/tests.js b/test/tests.js index 402be37..ca6d2cf 100644 --- a/test/tests.js +++ b/test/tests.js @@ -28,9 +28,27 @@ module('DS.LSAdapter', { App.Item.toString = stringify('App.Item'); + App.Order = DS.Model.extend({ + name: DS.attr('string'), + b: DS.attr('boolean'), + hours: DS.hasMany('hour') + }); + + App.Order.toString = stringify('App.Order'); + + App.Hour = DS.Model.extend({ + name: DS.attr('string'), + amount: DS.attr('number'), + order: DS.belongsTo('order') + }); + + App.Hour.toString = stringify('App.Hour'); + env = setupStore({ list: App.List, item: App.Item, + order: App.Order, + hour: App.Hour, adapter: DS.LSAdapter }); store = env.store; @@ -106,6 +124,39 @@ test('findAll', function() { }); }); +test('findQueryMany', function() { + expect(11); + stop(); + store.find('order', { b: true }).then(function(records) { + var firstRecord = records.objectAt(0), + secondRecord = records.objectAt(1), + thirdRecord = records.objectAt(2); + + equal(get(records, 'length'), 3, "3 orders were found"); + equal(get(firstRecord, 'name'), "one", "First order's name is one"); + equal(get(secondRecord, 'name'), "three", "Second order's name is three"); + equal(get(thirdRecord, 'name'), "four", "Third order's name is four"); + var firstHours = firstRecord.get('hours'), + secondHours = secondRecord.get('hours'), + thirdHours = thirdRecord.get('hours'); + + equal(get(firstHours, 'length'), 2, "Order one has two hours"); + equal(get(secondHours, 'length'), 2, "Order three has two hours"); + equal(get(thirdHours, 'length'), 0, "Order four has no hours"); + + var hourOne = firstHours.objectAt(0), + hourTwo = firstHours.objectAt(1), + hourThree = secondHours.objectAt(0), + hourFour = secondHours.objectAt(1); + equal(get(hourOne, 'amount'), 4, "Hour one has amount of 4"); + equal(get(hourTwo, 'amount'), 3, "Hour two has amount of 3"); + equal(get(hourThree, 'amount'), 2, "Hour three has amount of 2"); + equal(get(hourFour, 'amount'), 1, "Hour four has amount of 1"); + + start(); + }); +}); + test('createRecord', function() { expect(5); stop(); From 1533d206670bf4a75083f434169a991ee2e8cce5 Mon Sep 17 00:00:00 2001 From: bjarkehs Date: Thu, 13 Feb 2014 16:07:02 +0100 Subject: [PATCH 2/2] The readme shoud be updated to state that people need to define the serializer too. --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d59fd4b..93ff100 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,10 @@ Usage Include `localstorage_adapter.js` in your app and then like all adapters: ```js -App.LSAdapter = DS.LSAdapter.extend({ - namespace: 'app_namespace' +App.ApplicationSerializer = DS.LSSerializer.extend(); +App.ApplicationAdapter = DS.LSAdapter.extend({ + namespace: 'yournamespace' }); - -App.ApplicationAdapter = DS.LSAdapter; ``` ### Local Storage Namespace