diff --git a/addon/adapters/firebase.js b/addon/adapters/firebase.js index e594f811..15b0c2c8 100644 --- a/addon/adapters/firebase.js +++ b/addon/adapters/firebase.js @@ -197,12 +197,45 @@ export default DS.Adapter.extend(Ember.Evented, { }, fmt('DS: FirebaseAdapter#findAll %@ to %@', [type, ref.toString()])); }, - findQuery: function(store, type, query) { + findQuery: function(store, type, query, recordArray) { var adapter = this; var ref = this._getRef(type); - ref = this.applyQueryToRef(ref, query); + ref.on('child_added', function(snapshot) { + var record = store.recordForId(type, snapshot.key()); + + if (!record || !record.__listening) { + var payload = adapter._assignIdToPayload(snapshot); + var serializer = store.serializerFor(type); + adapter._updateRecordCacheForType(type, payload); + record = store.push(type, serializer.extractSingle(store, type, payload)); + } + + if (record) { + recordArray.addObject(record); + } + }); + + // `child_changed` is already handled by the record's + // value listener after a store.push. `child_moved` is + // a much less common case because it relates to priority + + ref.on('child_removed', function(snapshot) { + var record = store.recordForId(type, snapshot.key()); + if (record) { + recordArray.removeObject(record); + } + }); + + // clean up event handlers when the array is being destroyed + // so that future firebase events wont keep trying to use a + // destroyed store/serializer + recordArray.__firebaseCleanup = function () { + ref.off('child_added'); + ref.off('child_removed'); + }; + return new Promise(function(resolve, reject) { // Listen for child events on the type ref.once('value', function(snapshot) { diff --git a/addon/initializers/emberfire.js b/addon/initializers/emberfire.js index a83f4606..67b72795 100644 --- a/addon/initializers/emberfire.js +++ b/addon/initializers/emberfire.js @@ -75,6 +75,18 @@ export default { }); } + if (!DS.AdapterPopulatedRecordArray.prototype._emberfirePatched) { + DS.AdapterPopulatedRecordArray.reopen({ + _emberfirePatched: true, + willDestroy: function() { + if (this.__firebaseCleanup) { + this.__firebaseCleanup(); + } + return this._super(); + } + }); + } + DS.FirebaseAdapter = FirebaseAdapter; DS.FirebaseSerializer = FirebaseSerializer; } diff --git a/changelog.txt b/changelog.txt index e69de29b..2667b8ef 100644 --- a/changelog.txt +++ b/changelog.txt @@ -0,0 +1 @@ +feature - `store.find` with queries now returns a realtime array. diff --git a/config/ember-try.js b/config/ember-try.js index 96f221cd..91d37bf6 100644 --- a/config/ember-try.js +++ b/config/ember-try.js @@ -1,3 +1,4 @@ +/* jshint node: true */ module.exports = { scenarios: [ { diff --git a/tests/helpers/fixture-data.js b/tests/helpers/fixture-data.js index 3113d248..33e26a8c 100644 --- a/tests/helpers/fixture-data.js +++ b/tests/helpers/fixture-data.js @@ -9,6 +9,13 @@ export default { "post_1": true, "post_2": true } + }, + "esnowden": { + "firstName": "Edward", + "created": 1395162147634, + "posts": { + "post_3": true + } } }, "posts": { @@ -31,6 +38,16 @@ export default { "comment_4": true }, "title": "Post 2" + }, + "post_3": { + "published": 1395162147646, + "user": "esnowden", + "body": "This is the third FireBlog post!", + "comments": { + "comment_5": true, + "comment_6": true + }, + "title": "Post 3" } }, "comments": { @@ -53,9 +70,64 @@ export default { "published": 1395176007626, "user": "aputinski", "body": "This is a fourth comment" + }, + "comment_5": { + "published": 1395176007627, + "user": "esnowden", + "body": "This is a fifth comment" + }, + "comment_6": { + "published": 1395176007628, + "user": "esnowden", + "body": "This is a sixth comment" } } }, + "queries": { + "users": { + "tstirrat": { + "firstName": "Tim", + "created": 1395162147634, + "posts": { + "post_1": true, + "post_2": true, + "post_3": true + } + } + }, + "posts": { + "post_1": { + "published": 1395162147646, + "user": "tstirrat", + "body": "This is the first FireBlog post!", + "comments": { + "comment_1": true, + "comment_2": true + }, + "title": "Post 1" + }, + "post_2": { + "published": 1395162147646, + "user": "tstirrat", + "body": "This is the second FireBlog post!", + "comments": { + "comment_3": true, + "comment_4": true + }, + "title": "Post 2" + }, + "post_3": { + "published": 1395162147646, + "user": "tstirrat", + "body": "This is the third FireBlog post!", + "comments": { + "comment_3": true, + "comment_4": true + }, + "title": "Post 3" + } + }, + }, "denormalized": { "posts": { "post_1": { diff --git a/tests/integration/finding-records-test.js b/tests/integration/finding-records-test.js index 110ce1b2..463c78ec 100644 --- a/tests/integration/finding-records-test.js +++ b/tests/integration/finding-records-test.js @@ -144,7 +144,7 @@ describe("Integration: FirebaseAdapter - Finding Records", function() { Ember.run(function() { findAllPromise.then(function(payload) { assert(Ember.isArray(payload)); - assert.equal(payload.length, 2); + assert.equal(payload.length, 3); done(); }); }); diff --git a/tests/integration/queries-test.js b/tests/integration/queries-test.js new file mode 100644 index 00000000..834cd349 --- /dev/null +++ b/tests/integration/queries-test.js @@ -0,0 +1,82 @@ +import Ember from 'ember'; +import startApp from 'dummy/tests/helpers/start-app'; +import { it } from 'ember-mocha'; +import stubFirebase from 'dummy/tests/helpers/stub-firebase'; +import unstubFirebase from 'dummy/tests/helpers/unstub-firebase'; +import createTestRef from 'dummy/tests/helpers/create-test-ref'; + +describe('Integration: FirebaseAdapter - Queries', function() { + var app, store, adapter, findQueryArray, ref; + + beforeEach(function(done) { + stubFirebase(); + + app = startApp(); + + ref = createTestRef('blogs/queries'); + + store = app.__container__.lookup('store:main'); + adapter = store.adapterFor('application'); + adapter._ref = ref; + adapter._queueFlushDelay = false; + + var query = { limitToLast: 3 }; + + findQueryArray = store.recordArrayManager.createAdapterPopulatedRecordArray(store.modelFor('post'), query); + + Ember.run(function () { + adapter.findQuery(store, store.modelFor('post'), query, findQueryArray) + .then(() => { + done(); + }); + }); + }); + + afterEach(function() { + unstubFirebase(); + Ember.run(app, 'destroy'); + }); + + it('creates the correct Firebase reference', function() { + assert(ref.toString().match(/blogs\/queries$/g)); + }); + + it('resolves with the correct initial payload', function() { + assert(Ember.isArray(findQueryArray)); + assert.deepEqual(findQueryArray.get('content').mapBy('id'), ['post_1', 'post_2', 'post_3'], 'array should contain post_1, 2, 3'); + }); + + describe('when an item is added to the resultset', function () { + it('populates the item in the array', function(done) { + Ember.run(() => { + ref.child('posts/post_4').set({ title: 'Post 4', body: 'Body', published: 1395162147646, user: 'tstirrat' }, function () { + assert(findQueryArray.get('content').isAny('id', 'post_4'), 'post_4 should exist in the array'); + done(); + }); + }); + }); + }); + + describe('when an item is removed from the resultset', function () { + it('removes the item in the array', function(done) { + Ember.run(() => { + ref.child('posts/post_3').remove(function () { + assert(!findQueryArray.get('content').isAny('id', 'post_3'), 'post_3 should not exist in the array'); + done(); + }); + }); + }); + }); + + describe('when a resultset changes size', function () { + it('alters the array size', function(done) { + Ember.run(() => { + ref.child('posts/post_3').remove(function () { + assert.deepEqual(findQueryArray.get('content').mapBy('id'), ['post_1', 'post_2']); + done(); + }); + }); + }); + }); + +});