From ad3af829233721bd7fb9151a6078e54c157992c3 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Tue, 17 Jun 2014 16:30:02 -0700 Subject: [PATCH 1/5] Add support for updating multiple instances with query --- lib/connectors/memory.js | 23 +++++++++++++ lib/dao.js | 65 +++++++++++++++++++++++++++++++++++++ test/basic-querying.test.js | 34 +++++++++++++++++++ 3 files changed, 122 insertions(+) diff --git a/lib/connectors/memory.js b/lib/connectors/memory.js index ab54f959..de479c83 100644 --- a/lib/connectors/memory.js +++ b/lib/connectors/memory.js @@ -476,6 +476,29 @@ Memory.prototype.count = function count(model, callback, where) { }); }; +Memory.prototype.update = + Memory.prototype.updateAll = function updateAll(model, where, data, cb) { + var self = this; + var cache = this.cache[model]; + var filter = null; + where = where || {}; + filter = applyFilter({where: where}); + + var ids = Object.keys(cache); + async.each(ids, function (id, done) { + var inst = self.fromDb(model, cache[id]); + if (!filter || filter(inst)) { + self.updateAttributes(model, id, data, done); + } else { + process.nextTick(done); + } + }, function (err) { + if (!err) { + self.saveToFile(null, cb); + } + }); + }; + Memory.prototype.updateAttributes = function updateAttributes(model, id, data, cb) { if (!id) { var err = new Error('You must provide an id when updating attributes!'); diff --git a/lib/dao.js b/lib/dao.js index d4bd40de..5f8d0c77 100644 --- a/lib/dao.js +++ b/lib/dao.js @@ -19,6 +19,7 @@ var utils = require('./utils'); var fieldsToArray = utils.fieldsToArray; var removeUndefined = utils.removeUndefined; var util = require('util'); +var assert = require('assert'); /** * Base class for all persistent objects. @@ -961,6 +962,70 @@ DataAccessObject.prototype.save = function (options, callback) { } }; +/** + * Update multiple instances that match the where clause + * + * Example: + * + *```js + * Employee.update({managerId: 'x001'}, {managerId: 'x002'}, function(err) { + * ... + * }); + * ``` + * + * @param {Object} [where] Search conditions (optional) + * @param {Object} data Changes to be made + * @param {Function} cb Callback, called with (err, count) + */ +DataAccessObject.update = +DataAccessObject.updateAll = function (where, data, cb) { + if (stillConnecting(this.getDataSource(), this, arguments)) return; + + if (arguments.length === 1) { + // update(data); + data = where; + where = null; + cb = null; + } else if (arguments.length === 2) { + if (typeof data === 'function') { + // update(data, cb); + cb = data; + data = where; + where = null; + } else { + // update(where, data); + cb = null; + } + } + + assert(typeof where === 'object', 'The where argument should be an object'); + assert(typeof data === 'object', 'The data argument should be an object'); + assert(cb === null || typeof cb === 'function', 'The data argument should be an object'); + + try { + where = removeUndefined(where); + where = this._coerce(where); + } catch (err) { + return process.nextTick(function () { + cb && cb(err); + }); + } + var connector = this.getDataSource().connector; + connector.update(this.modelName, where, data, cb); +}; + +// updateAll ~ remoting attributes +setRemoting(DataAccessObject.updateAll, { + description: 'Update instances of the model matched by where from the data source', + accepts: [ + {arg: 'where', type: 'object', http: {source: 'query'}, + description: 'Criteria to match model instances'}, + {arg: 'data', type: 'object', http: {source: 'body'}, + description: 'An object of model property name/value pairs'}, + ], + http: {verb: 'post', path: '/update'} +}); + DataAccessObject.prototype.isNewRecord = function () { return !getIdValue(this.constructor, this); }; diff --git a/test/basic-querying.test.js b/test/basic-querying.test.js index eaccb348..12ac4dcf 100644 --- a/test/basic-querying.test.js +++ b/test/basic-querying.test.js @@ -541,6 +541,40 @@ describe('basic-querying', function () { }); + describe('updateAll ', function () { + + beforeEach(seed); + + it('should only update instances that satisfy the where condition', function (done) { + User.update({name: 'John Lennon'}, {name: 'John Smith'}, function () { + User.find({where: {name: 'John Lennon'}}, function (err, data) { + should.not.exist(err); + data.length.should.equal(0); + User.find({where: {name: 'John Smith'}}, function (err, data) { + should.not.exist(err); + data.length.should.equal(1); + done(); + }); + }); + }); + }); + + it('should update all instances without where', function (done) { + User.update({name: 'John Smith'}, function () { + User.find({where: {name: 'John Lennon'}}, function (err, data) { + should.not.exist(err); + data.length.should.equal(0); + User.find({where: {name: 'John Smith'}}, function (err, data) { + should.not.exist(err); + data.length.should.equal(6); + done(); + }); + }); + }); + }); + + }); + }); function seed(done) { From a487eb57cd47eb4bef604c1d11d006601dcfe978 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Tue, 17 Jun 2014 23:19:28 -0700 Subject: [PATCH 2/5] Add like/nlike support for memory connector --- lib/connectors/memory.js | 22 +++++++++- test/memory.test.js | 93 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 1 deletion(-) diff --git a/lib/connectors/memory.js b/lib/connectors/memory.js index de479c83..e86d7d27 100644 --- a/lib/connectors/memory.js +++ b/lib/connectors/memory.js @@ -363,8 +363,12 @@ function applyFilter(filter) { return pass; } + function replaceAll(string, find, replace) { + return string.replace(new RegExp(find, 'g'), replace); + } + function test(example, value) { - if (typeof value === 'string' && example && example.constructor.name === 'RegExp') { + if (typeof value === 'string' && (example instanceof RegExp)) { return value.match(example); } if (example === undefined || value === undefined) { @@ -386,6 +390,22 @@ function applyFilter(filter) { return false; } + if (example.like || example.nlike) { + + var like = example.like || example.nlike; + if (typeof like === 'string') { + like = replaceAll(like, '%', '.*'); + like = replaceAll(like, '_', '.'); + } + if (example.like) { + return !!new RegExp(like).test(value); + } + + if (example.nlike) { + return !new RegExp(like).test(value); + } + } + if (testInEquality(example, value)) { return true; } diff --git a/test/memory.test.js b/test/memory.test.js index 4c613270..b98462a1 100644 --- a/test/memory.test.js +++ b/test/memory.test.js @@ -4,6 +4,7 @@ var path = require('path'); var fs = require('fs'); var assert = require('assert'); var async = require('async'); +var should = require('./init.js'); describe('Memory connector', function () { var file = path.join(__dirname, 'memory.json'); @@ -91,5 +92,97 @@ describe('Memory connector', function () { }); }); + + describe('Query for memory connector', function () { + var ds = new DataSource({ + connector: 'memory' + }); + + var User = ds.define('User', { + seq: {type: Number, index: true}, + name: {type: String, index: true, sort: true}, + email: {type: String, index: true}, + birthday: {type: Date, index: true}, + role: {type: String, index: true}, + order: {type: Number, index: true, sort: true}, + vip: {type: Boolean} + }); + + before(seed); + it('should allow to find using like', function (done) { + User.find({where: {name: {like: '%St%'}}}, function (err, posts) { + should.not.exist(err); + posts.should.have.property('length', 2); + done(); + }); + }); + + it('should support like for no match', function (done) { + User.find({where: {name: {like: 'M%XY'}}}, function (err, posts) { + should.not.exist(err); + posts.should.have.property('length', 0); + done(); + }); + }); + + it('should allow to find using nlike', function (done) { + User.find({where: {name: {nlike: '%St%'}}}, function (err, posts) { + should.not.exist(err); + posts.should.have.property('length', 4); + done(); + }); + }); + + it('should support nlike for no match', function (done) { + User.find({where: {name: {nlike: 'M%XY'}}}, function (err, posts) { + should.not.exist(err); + posts.should.have.property('length', 6); + done(); + }); + }); + + function seed(done) { + var count = 0; + var beatles = [ + { + seq: 0, + name: 'John Lennon', + email: 'john@b3atl3s.co.uk', + role: 'lead', + birthday: new Date('1980-12-08'), + order: 2, + vip: true + }, + { + seq: 1, + name: 'Paul McCartney', + email: 'paul@b3atl3s.co.uk', + role: 'lead', + birthday: new Date('1942-06-18'), + order: 1, + vip: true + }, + {seq: 2, name: 'George Harrison', order: 5, vip: false}, + {seq: 3, name: 'Ringo Starr', order: 6, vip: false}, + {seq: 4, name: 'Pete Best', order: 4}, + {seq: 5, name: 'Stuart Sutcliffe', order: 3, vip: true} + ]; + User.destroyAll(function () { + beatles.forEach(function (beatle) { + User.create(beatle, ok); + }); + }); + + function ok() { + if (++count === beatles.length) { + done(); + } + } + } + + }); + }); + + From b3b29d731382eeaec2763c4052f8a988f6168bb4 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Wed, 18 Jun 2014 12:37:49 -0700 Subject: [PATCH 3/5] Enhance the wildcard to regexp conversion --- lib/connectors/memory.js | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/lib/connectors/memory.js b/lib/connectors/memory.js index e86d7d27..06c65772 100644 --- a/lib/connectors/memory.js +++ b/lib/connectors/memory.js @@ -363,8 +363,34 @@ function applyFilter(filter) { return pass; } - function replaceAll(string, find, replace) { - return string.replace(new RegExp(find, 'g'), replace); + function toRegExp(pattern) { + if (pattern instanceof RegExp) { + return pattern; + } + var regex = ''; + pattern = pattern.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"); + for (var i = 0, n = pattern.length; i < n; i++) { + var char = pattern.charAt(i); + if (char === '\\') { + i++; // Skip to next char + if (i < n) { + regex += pattern.charAt(i); + } + continue; + } else if (char === '%') { + regex += '.*'; + } else if (char === '_') { + regex += '.'; + } else if (char === '.') { + regex += '\\.'; + } else if (char === '*') { + regex += '\\*'; + } + else { + regex += char; + } + } + return regex; } function test(example, value) { @@ -394,8 +420,7 @@ function applyFilter(filter) { var like = example.like || example.nlike; if (typeof like === 'string') { - like = replaceAll(like, '%', '.*'); - like = replaceAll(like, '_', '.'); + like = toRegExp(like); } if (example.like) { return !!new RegExp(like).test(value); From a1836662a78e1338553b5d212ad336d3f3efc5fc Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Fri, 20 Jun 2014 12:05:32 -0700 Subject: [PATCH 4/5] Clean up comments --- lib/connectors/memory.js | 2 ++ lib/dao.js | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/connectors/memory.js b/lib/connectors/memory.js index 06c65772..97531f13 100644 --- a/lib/connectors/memory.js +++ b/lib/connectors/memory.js @@ -368,6 +368,8 @@ function applyFilter(filter) { return pattern; } var regex = ''; + // Escaping user input to be treated as a literal string within a regular expression + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Writing_a_Regular_Expression_Pattern pattern = pattern.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"); for (var i = 0, n = pattern.length; i < n; i++) { var char = pattern.charAt(i); diff --git a/lib/dao.js b/lib/dao.js index 5f8d0c77..c788413e 100644 --- a/lib/dao.js +++ b/lib/dao.js @@ -982,25 +982,25 @@ DataAccessObject.updateAll = function (where, data, cb) { if (stillConnecting(this.getDataSource(), this, arguments)) return; if (arguments.length === 1) { - // update(data); + // update(data) is being called data = where; where = null; cb = null; } else if (arguments.length === 2) { if (typeof data === 'function') { - // update(data, cb); + // update(data, cb) is being called cb = data; data = where; where = null; } else { - // update(where, data); + // update(where, data) is being called cb = null; } } assert(typeof where === 'object', 'The where argument should be an object'); assert(typeof data === 'object', 'The data argument should be an object'); - assert(cb === null || typeof cb === 'function', 'The data argument should be an object'); + assert(cb === null || typeof cb === 'function', 'The cb argument should be a function'); try { where = removeUndefined(where); From b07c36eab7b3fe5f889d8e55b20dda9585d9d975 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Fri, 20 Jun 2014 12:05:42 -0700 Subject: [PATCH 5/5] Use async for flow control --- test/basic-querying.test.js | 16 ++++++---------- test/memory.test.js | 15 +++++---------- 2 files changed, 11 insertions(+), 20 deletions(-) diff --git a/test/basic-querying.test.js b/test/basic-querying.test.js index 12ac4dcf..28fa210b 100644 --- a/test/basic-querying.test.js +++ b/test/basic-querying.test.js @@ -1,5 +1,6 @@ // This test written in mocha+should.js var should = require('./init.js'); +var async = require('async'); var db, User; describe('basic-querying', function () { @@ -578,7 +579,6 @@ describe('basic-querying', function () { }); function seed(done) { - var count = 0; var beatles = [ { seq: 0, @@ -603,15 +603,11 @@ function seed(done) { {seq: 4, name: 'Pete Best', order: 4}, {seq: 5, name: 'Stuart Sutcliffe', order: 3, vip: true} ]; - User.destroyAll(function () { - beatles.forEach(function (beatle) { - User.create(beatle, ok); - }); - }); - function ok() { - if (++count === beatles.length) { - done(); + async.series([ + User.destroyAll.bind(User), + function(cb) { + async.each(beatles, User.create.bind(User), cb); } - } + ], done); } diff --git a/test/memory.test.js b/test/memory.test.js index b98462a1..4016209e 100644 --- a/test/memory.test.js +++ b/test/memory.test.js @@ -142,7 +142,6 @@ describe('Memory connector', function () { }); function seed(done) { - var count = 0; var beatles = [ { seq: 0, @@ -167,17 +166,13 @@ describe('Memory connector', function () { {seq: 4, name: 'Pete Best', order: 4}, {seq: 5, name: 'Stuart Sutcliffe', order: 3, vip: true} ]; - User.destroyAll(function () { - beatles.forEach(function (beatle) { - User.create(beatle, ok); - }); - }); - function ok() { - if (++count === beatles.length) { - done(); + async.series([ + User.destroyAll.bind(User), + function(cb) { + async.each(beatles, User.create.bind(User), cb); } - } + ], done); } });