From eef9b6015cbd07024b1dcfb6ebbd3d19cf7afe0f Mon Sep 17 00:00:00 2001 From: jannyHou Date: Fri, 15 Nov 2019 16:28:00 -0500 Subject: [PATCH] feat: add partitioned find --- lib/cloudant.js | 50 ++++++++ package.json | 5 +- test/partition.test.js | 268 ++++++++++++++++++++++++----------------- 3 files changed, 211 insertions(+), 112 deletions(-) diff --git a/lib/cloudant.js b/lib/cloudant.js index 97bc5c7b..e71b6531 100644 --- a/lib/cloudant.js +++ b/lib/cloudant.js @@ -133,6 +133,56 @@ Cloudant.prototype.ping = function(cb) { else cb(); } }; + +/** + * Apply find queries function + * + * @param {Object} mo The selected model + * @param {Object} query The query to filter + * @param {Object[]} docs Model document/data + * @param {Object} include Include filter + * @callback {Function} cb The callback function + */ +Cloudant.prototype._findRecursive = function( + mo, query, docs, include, options, cb, +) { + const self = this; + const db = this.cloudant.use(self.getDbName(self)); + const partitionKey = options && options.partitionKey; + debug('Cloudant _findRecursive: partitionKey: %s', partitionKey); + + const findCb = function(err, rst) { + debug('Cloudant.prototype.all (findRecursive) results: %j %j', err, rst); + if (err) return cb(err); + + // only sort numeric id if the id type is of Number + const idName = self.getIdName(mo.mo.model.modelName); + if (!!idName && mo.mo.properties[idName].type.name === 'Number' && + query.sort) + self._sortNumericId(rst.docs, query.sort); + + // work around for issue + // https://github.com/strongloop/loopback-connector-Couchdb/issues/73 + if (!rst.docs) { + const queryView = util.inspect(query, 4); + debug('findRecursive query: %s', queryView); + const errMsg = util.format('No documents returned for query: %s', + queryView); + return cb(new Error(g.f(errMsg))); + } + include(rst.docs, function(err) { + if (err) return cb(err); + self._extendDocs(rst, docs, query, mo, include, options, cb); + }); + }; + + if (partitionKey) { + db.partitionedFind(partitionKey, query, findCb); + } else { + mo.db.find(query, findCb); + } +}; + // mixins require('./view')(Cloudant); require('./geo')(Cloudant); diff --git a/package.json b/package.json index c4940c94..6364561f 100644 --- a/package.json +++ b/package.json @@ -29,9 +29,10 @@ "fs-extra": "^8.1.0", "lodash": "^4.17.11", "loopback-connector": "^4.0.0", - "loopback-connector-couchdb2": "^1.5.1", + "loopback-connector-couchdb2": "^1.5.2", "request": "^2.81.0", - "strong-globalize": "^5.0.0" + "strong-globalize": "^5.0.0", + "uuid": "^3.3.3" }, "devDependencies": { "dockerode": "^2.4.3", diff --git a/test/partition.test.js b/test/partition.test.js index 810d5173..5c29fa02 100644 --- a/test/partition.test.js +++ b/test/partition.test.js @@ -8,6 +8,7 @@ require('./init.js'); const _ = require('lodash'); const should = require('should'); +const uuid = require('uuid/v4'); const DEFAULT_MODEL_VIEW = 'loopback__model__name'; let Product, db, connector; @@ -28,141 +29,188 @@ describe('cloudant - partitioned db', () => { db.automigrate(done); }); - it('property level - create global index by default', (done) => { - Product = db.define('Product', { - prodName: {type: String, index: true}, - desc: {type: String}, - }); - db.autoupdate('Product', (err) => { - if (err) return done(err); - connector.getIndexes(connector.getDbName(connector), (e, results) => { - if (e) return done(e); - const indexes = results.indexes; - const indexName = 'prodName_index'; - should.exist(indexes); + context('index tests', ()=> { + it('property level - create global index by default', (done) => { + Product = db.define('Product', { + prodName: {type: String, index: true}, + desc: {type: String}, + }); + db.autoupdate('Product', (err) => { + if (err) return done(err); + connector.getIndexes(connector.getDbName(connector), (e, results) => { + if (e) return done(e); + const indexes = results.indexes; + const indexName = 'prodName_index'; + should.exist(indexes); - const index = _.find(indexes, function(index) { - return index.name === indexName; + const index = _.find(indexes, function(index) { + return index.name === indexName; + }); + should.exist(index); + index.name.should.equal(indexName); + index.def.fields[0]['prodName'].should.equal('asc'); + index.def.fields[1][DEFAULT_MODEL_VIEW].should.equal('asc'); + // should be a global index + index.partitioned.should.equal(false); + done(); }); - should.exist(index); - index.name.should.equal(indexName); - index.def.fields[0]['prodName'].should.equal('asc'); - index.def.fields[1][DEFAULT_MODEL_VIEW].should.equal('asc'); - // should be a global index - index.partitioned.should.equal(false); - done(); }); }); - }); - it('index entry - create global index by default', (done) => { - Product = db.define('Product', { - prodName: {type: String}, - desc: {type: String}, - }, { - indexes: { - 'prodName1_index': { - keys: { - prodName: -1, + it('index entry - create global index by default', (done) => { + Product = db.define('Product', { + prodName: {type: String}, + desc: {type: String}, + }, { + indexes: { + 'prodName1_index': { + keys: { + prodName: -1, + }, }, }, - }, - }); - db.autoupdate('Product', (err) => { - if (err) return done(err); - connector.getIndexes(connector.getDbName(connector), (e, results) => { - if (e) return done(e); - const indexes = results.indexes; - const indexName = 'prodName1_index'; - should.exist(indexes); + }); + db.autoupdate('Product', (err) => { + if (err) return done(err); + connector.getIndexes(connector.getDbName(connector), (e, results) => { + if (e) return done(e); + const indexes = results.indexes; + const indexName = 'prodName1_index'; + should.exist(indexes); - const index = _.find(indexes, function(index) { - return index.name === indexName; + const index = _.find(indexes, function(index) { + return index.name === indexName; + }); + should.exist(index); + index.name.should.equal(indexName); + index.def.fields[0]['prodName'].should.equal('desc'); + index.def.fields[1][DEFAULT_MODEL_VIEW].should.equal('desc'); + // should be a global index + index.partitioned.should.equal(false); + done(); }); - should.exist(index); - index.name.should.equal(indexName); - index.def.fields[0]['prodName'].should.equal('desc'); - index.def.fields[1][DEFAULT_MODEL_VIEW].should.equal('desc'); - // should be a global index - index.partitioned.should.equal(false); - done(); }); }); - }); - it('index entry - ' + - 'create partitioned index for when `partitioned` is configured as true', - (done) => { - Product = db.define('Product', { - prodName: {type: String}, - desc: {type: String}, - }, { - indexes: { - 'prodName2_index': { - partitioned: true, - keys: { - prodName: 1, + it('index entry - ' + + 'create partitioned index for when `partitioned` is configured as true', + (done) => { + Product = db.define('Product', { + prodName: {type: String}, + desc: {type: String}, + }, { + indexes: { + 'prodName2_index': { + partitioned: true, + keys: { + prodName: 1, + }, }, }, - }, - }); - db.autoupdate('Product', (err) => { - if (err) return done(err); - connector.getIndexes(connector.getDbName(connector), (e, results) => { - if (e) return done(e); - const indexes = results.indexes; - const indexName = 'prodName2_index'; - should.exist(indexes); + }); + db.autoupdate('Product', (err) => { + if (err) return done(err); + connector.getIndexes(connector.getDbName(connector), (e, results) => { + if (e) return done(e); + const indexes = results.indexes; + const indexName = 'prodName2_index'; + should.exist(indexes); - const index = _.find(indexes, function(index) { - return index.name === indexName; + const index = _.find(indexes, function(index) { + return index.name === indexName; + }); + should.exist(index); + index.name.should.equal(indexName); + index.def.fields[0]['prodName'].should.equal('asc'); + index.def.fields[1][DEFAULT_MODEL_VIEW].should.equal('asc'); + // should be a global index + index.partitioned.should.equal(true); + done(); }); - should.exist(index); - index.name.should.equal(indexName); - index.def.fields[0]['prodName'].should.equal('asc'); - index.def.fields[1][DEFAULT_MODEL_VIEW].should.equal('asc'); - // should be a global index - index.partitioned.should.equal(true); - done(); }); }); - }); - it('index entry - ' + - 'create global index for when `partitioned` is configured as false', - (done) => { - Product = db.define('Product', { - prodName: {type: String}, - desc: {type: String}, - }, { - indexes: { - 'prodName3_index': { - partitioned: false, - keys: { - prodName: 1, + it('index entry - ' + + 'create global index for when `partitioned` is configured as false', + (done) => { + Product = db.define('Product', { + prodName: {type: String}, + desc: {type: String}, + }, { + indexes: { + 'prodName3_index': { + partitioned: false, + keys: { + prodName: 1, + }, }, }, - }, + }); + db.automigrate('Product', (err) => { + if (err) return done(err); + connector.getIndexes(connector.getDbName(connector), (e, results) => { + if (e) return done(e); + const indexes = results.indexes; + const indexName = 'prodName3_index'; + should.exist(indexes); + + const index = _.find(indexes, function(index) { + return index.name === indexName; + }); + should.exist(index); + index.name.should.equal(indexName); + index.def.fields[0]['prodName'].should.equal('asc'); + index.def.fields[1][DEFAULT_MODEL_VIEW].should.equal('asc'); + // should be a global index + index.partitioned.should.equal(false); + done(); + }); + }); }); - db.automigrate('Product', (err) => { - if (err) return done(err); - connector.getIndexes(connector.getDbName(connector), (e, results) => { - if (e) return done(e); - const indexes = results.indexes; - const indexName = 'prodName3_index'; - should.exist(indexes); + }); - const index = _.find(indexes, function(index) { - return index.name === indexName; + context('findAll tests', ()=> { + it('find all records by partition key from option', (done) => { + Product = db.define('Product', { + name: {type: String}, + tag: {type: String}, + }, { + forceId: false, + indexes: { + 'product_name_index': { + partitioned: true, + keys: { + city: 1, + }, + }, + }, + }); + db.automigrate('Product', () => { + Product.create(SEED_DATA, function(err) { + if (err) return done(err); + Product.find({where: {tag: 'food'}}, {partitionKey: 'toronto'}, + (err, results) => { + if (err) return done(err); + should.exist(results); + results.length.should.equal(2); + const resultTaggedFood = _.filter(results, + function(r) { + return r.tag === 'food'; + }); + resultTaggedFood.length.should.equal(2); + done(); + }); }); - should.exist(index); - index.name.should.equal(indexName); - index.def.fields[0]['prodName'].should.equal('asc'); - index.def.fields[1][DEFAULT_MODEL_VIEW].should.equal('asc'); - // should be a global index - index.partitioned.should.equal(false); - done(); }); }); }); }); + +const SEED_DATA = [ + {id: `toronto: ${uuid()}`, name: 'beer', tag: 'drink'}, + {id: `toronto: ${uuid()}`, name: 'salad', tag: 'food'}, + {id: `toronto: ${uuid()}`, name: 'soup', tag: 'food'}, + {id: `london: ${uuid()}`, name: 'beer', tag: 'drink'}, + {id: `london: ${uuid()}`, name: 'salad', tag: 'food'}, + {id: `london: ${uuid()}`, name: 'salad', tag: 'food'}, +];