diff --git a/addon/adapters/pouch.js b/addon/adapters/pouch.js index cff8735..310ea60 100644 --- a/addon/adapters/pouch.js +++ b/addon/adapters/pouch.js @@ -202,6 +202,58 @@ export default DS.RESTAdapter.extend({ return data; }, + /** + * Return key that conform to data adapter + * ex: 'name' become 'data.name' + */ + _dataKey: function(key) { + var dataKey ='data.' + key; + return ""+ dataKey + ""; + }, + + /** + * Returns the modified selector key to comform data key + * Ex: selector: {name: 'Mario'} wil become selector: {'data.name': 'Mario'} + */ + _buildSelector: function(selector) { + var dataSelector = {}; + var selectorKeys = []; + + for (var key in selector) { + if(selector.hasOwnProperty(key)){ + selectorKeys.push(key); + } + } + + selectorKeys.forEach(function(key) { + var dataKey = this._dataKey(key); + dataSelector[dataKey] = selector[key]; + }.bind(this)); + + return dataSelector; + }, + + /** + * Returns the modified sort key + * Ex: sort: ['series'] will become ['data.series'] + * Ex: sort: [{series: 'desc'}] will became [{'data.series': 'desc'}] + */ + _buildSort: function(sort) { + return sort.map(function (value) { + var sortKey = {}; + if (typeof value === 'object' && value !== null) { + for (var key in value) { + if(value.hasOwnProperty(key)){ + sortKey[this._dataKey(key)] = value[key]; + } + } + } else { + return this._dataKey(value); + } + return sortKey; + }.bind(this)); + }, + /** * Returns the string to use for the model name part of the PouchDB document * ID for records of the given ember-data type. @@ -229,10 +281,43 @@ export default DS.RESTAdapter.extend({ return this.get('db').rel.find(this.getRecordTypeName(type), ids); }, - findQuery: function(/* store, type, query */) { - throw new Error( - "findQuery not yet supported by ember-pouch. " + - "See https://github.com/nolanlawson/ember-pouch/issues/7."); + + query: function(store, type, query) { + this._init(store, type); + + var recordTypeName = this.getRecordTypeName(type); + var db = this.get('db'); + + var queryParams = { + selector: this._buildSelector(query.filter) + }; + + if (!Ember.isEmpty(query.sort)) { + queryParams.sort = this._buildSort(query.sort); + } + + return db.find(queryParams).then(function (payload) { + if (typeof payload === 'object' && payload !== null) { + var plural = pluralize(recordTypeName); + var results = {}; + + var rows = payload.docs.map((row) => { + var parsedId = db.rel.parseDocID(row._id); + if (!Ember.isEmpty(parsedId.id)) { + row.data.id = parsedId.id; + return row.data; + } + }); + + results[plural] = rows; + + return results; + } + }); + }, + + queryRecord: function(store, type, query) { + return this.query(store, type, query); }, /** diff --git a/blueprints/ember-pouch/index.js b/blueprints/ember-pouch/index.js index decd989..c96b647 100644 --- a/blueprints/ember-pouch/index.js +++ b/blueprints/ember-pouch/index.js @@ -1,4 +1,5 @@ 'use strict'; + "pouchdb-find": "^0.10.2" module.exports = { normalizeEntityName: function() {}, @@ -6,7 +7,8 @@ module.exports = { afterInstall: function() { return this.addBowerPackagesToProject([ { name: 'pouchdb', target: '^5.4.5' }, - { name: 'relational-pouch', target: '^1.4.4'} + { name: 'relational-pouch', target: '^1.4.4'}, + { name: 'pouchdb-find', target: '^0.10.2'} ]); } }; diff --git a/bower.json b/bower.json index a07d5e0..e32153e 100644 --- a/bower.json +++ b/bower.json @@ -35,6 +35,7 @@ "qunit": "~1.20.0", "pouchdb": "^5.4.5", "relational-pouch": "^1.4.4", - "phantomjs-polyfill-object-assign": "chuckplantain/phantomjs-polyfill-object-assign" + "phantomjs-polyfill-object-assign": "chuckplantain/phantomjs-polyfill-object-assign", + "pouchdb-find": "^0.10.2" } } diff --git a/index.js b/index.js index 92b5254..d0adc63 100644 --- a/index.js +++ b/index.js @@ -32,6 +32,7 @@ module.exports = { app.import(bowerDir + '/pouchdb/dist/pouchdb.js'); app.import(bowerDir + '/relational-pouch/dist/pouchdb.relational-pouch.js'); + app.import(bowerDir + '/pouchdb-find/dist/pouchdb.find.js'); app.import('vendor/ember-pouch/shim.js', { type: 'vendor', exports: { diff --git a/tests/dummy/app/models/smasher.js b/tests/dummy/app/models/smasher.js new file mode 100644 index 0000000..b8eca69 --- /dev/null +++ b/tests/dummy/app/models/smasher.js @@ -0,0 +1,10 @@ +import DS from 'ember-data'; + +export default DS.Model.extend({ + rev: DS.attr('string'), + + name: DS.attr('string'), + series: DS.attr('string'), + debut: DS.attr(), +}); + diff --git a/tests/integration/adapters/pouch-basics-test.js b/tests/integration/adapters/pouch-basics-test.js index b92973b..6612161 100644 --- a/tests/integration/adapters/pouch-basics-test.js +++ b/tests/integration/adapters/pouch-basics-test.js @@ -59,6 +59,137 @@ test('can find one', function (assert) { }); }); +test('can query with sort', function (assert) { + var done = assert.async(); + Ember.RSVP.Promise.resolve().then(() => { + return this.db().createIndex({ index: { + fields: ['data.name'] } + }).then(() => { + return this.db().bulkDocs([ + { _id: 'smasher_2_mario', data: { name: 'Mario', series: 'Mario', debut: 1981 }}, + { _id: 'smasher_2_puff', data: { name: 'Jigglypuff', series: 'Pokemon', debut: 1996 }}, + { _id: 'smasher_2_link', data: { name: 'Link', series: 'Zelda', debut: 1986 }}, + { _id: 'smasher_2_dk', data: { name: 'Donkey Kong', series: 'Mario', debut: 1981 }}, + { _id: 'smasher_2_pika', data: { name: 'Pikachu', series: 'Pokemon', _id: 'pikachu', debut: 1996 }} + ]); + }); + }).then(() => { + return this.store().query('smasher', { + filter: {name: {$gt: ''}}, + sort: ['name'] + }); + }).then((found) => { + assert.equal(found.get('length'), 5, 'should returns all the smashers '); + assert.deepEqual(found.mapBy('id'), ['dk','puff','link','mario','pika'], + 'should have extracted the IDs correctly'); + assert.deepEqual(found.mapBy('name'), ['Donkey Kong', 'Jigglypuff', 'Link', 'Mario','Pikachu'], + 'should have extracted the attributes also'); + done(); + }).catch((error) => { + console.error('error in test', error); + assert.ok(false, 'error in test:' + error); + done(); + }); +}); + +test('can query multi-field queries', function (assert) { + var done = assert.async(); + Ember.RSVP.Promise.resolve().then(() => { + return this.db().createIndex({ index: { + fields: ['data.series', 'data.debut'] } + }).then(() => { + return this.db().bulkDocs([ + { _id: 'smasher_2_mario', data: { name: 'Mario', series: 'Mario', debut: 1981 }}, + { _id: 'smasher_2_puff', data: { name: 'Jigglypuff', series: 'Pokemon', debut: 1996 }}, + { _id: 'smasher_2_link', data: { name: 'Link', series: 'Zelda', debut: 1986 }}, + { _id: 'smasher_2_dk', data: { name: 'Donkey Kong', series: 'Mario', debut: 1981 }}, + { _id: 'smasher_2_pika', data: { name: 'Pikachu', series: 'Pokemon', _id: 'pikachu', debut: 1996 }} + ]); + }); + }).then(() => { + return this.store().query('smasher', { + filter: {series: 'Mario' }, + sort: [ + {series: 'desc'}, + {debut: 'desc'}] + }); + }).then((found) => { + assert.equal(found.get('length'), 2, 'should have found the two smashers'); + assert.deepEqual(found.mapBy('id'), ['mario', 'dk'], + 'should have extracted the IDs correctly'); + assert.deepEqual(found.mapBy('name'), ['Mario', 'Donkey Kong'], + 'should have extracted the attributes also'); + done(); + }).catch((error) => { + console.error('error in test', error); + assert.ok(false, 'error in test:' + error); + done(); + }); +}); + +test('can query one record', function (assert) { + var done = assert.async(); + Ember.RSVP.Promise.resolve().then(() => { + return this.db().createIndex({ index: { + fields: ['data.flavor'] } + }).then(() => { + return this.db().bulkDocs([ + { _id: 'tacoSoup_2_C', data: { flavor: 'al pastor', ingredients: ['X', 'Y'] } }, + { _id: 'tacoSoup_2_D', data: { flavor: 'black bean', ingredients: ['Z'] } }, + { _id: 'foodItem_2_X', data: { name: 'pineapple' }}, + { _id: 'foodItem_2_Y', data: { name: 'pork loin' }}, + { _id: 'foodItem_2_Z', data: { name: 'black beans' }} + ]); + }); + }).then(() => { + return this.store().queryRecord('taco-soup', { + filter: {flavor: 'al pastor' } + }); + }).then((found) => { + assert.equal(found.get('flavor'), 'al pastor', + 'should have found the requested item'); + done(); + }).catch((error) => { + console.error('error in test', error); + assert.ok(false, 'error in test:' + error); + done(); + }); +}); + +test('can query one associated records', function (assert) { + var done = assert.async(); + Ember.RSVP.Promise.resolve().then(() => { + return this.db().createIndex({ index: { + fields: ['data.flavor'] } + }).then(() => { + return this.db().bulkDocs([ + { _id: 'tacoSoup_2_C', data: { flavor: 'al pastor', ingredients: ['X', 'Y'] } }, + { _id: 'tacoSoup_2_D', data: { flavor: 'black bean', ingredients: ['Z'] } }, + { _id: 'foodItem_2_X', data: { name: 'pineapple' }}, + { _id: 'foodItem_2_Y', data: { name: 'pork loin' }}, + { _id: 'foodItem_2_Z', data: { name: 'black beans' }} + ]); + }); + }).then(() => { + return this.store().queryRecord('taco-soup', { + filter: {flavor: 'al pastor' }}); + }).then((found) => { + assert.equal(found.get('flavor'), 'al pastor', + 'should have found the requested item'); + return found.get('ingredients'); + }).then((foundIngredients) => { + assert.deepEqual(foundIngredients.mapBy('id'), ['X', 'Y'], + 'should have found both associated items'); + assert.deepEqual(foundIngredients.mapBy('name'), ['pineapple', 'pork loin'], + 'should have fully loaded the associated items'); + done(); + }).catch((error) => { + console.error('error in test', error); + assert.ok(false, 'error in test:' + error); + done(); + }); +}); + test('can find associated records', function (assert) { assert.expect(3);