diff --git a/lib/mysql.js b/lib/mysql.js index f132ffa..c8ed918 100644 --- a/lib/mysql.js +++ b/lib/mysql.js @@ -203,7 +203,7 @@ MySQL.prototype.create = function (model, data, callback) { * @param {Object} data The model instance data * @param {Function} [callback] The callback function */ -MySQL.prototype.updateOrCreate = function (model, data, callback) { +MySQL.prototype.updateOrCreate = MySQL.prototype.save = function (model, data, callback) { var mysql = this; var fieldsNames = []; var fieldValues = []; @@ -244,7 +244,9 @@ MySQL.prototype.toFields = function (model, data) { Object.keys(data).forEach(function (key) { if (props[key]) { var value = this.toDatabase(props[key], data[key]); - if ('undefined' === typeof value) return; + if (undefined === value) { + return; + } fields.push(self.columnEscaped(model, key) + ' = ' + value); } }.bind(this)); @@ -271,8 +273,9 @@ function dateToMysql(val) { * @returns {*} */ MySQL.prototype.toDatabase = function (prop, val) { - if (val === null) return 'NULL'; - if (val === undefined) return 'NULL'; + if (val === null || val === undefined) { + return 'NULL'; + } if (val.constructor.name === 'Object') { var operator = Object.keys(val)[0] val = val[operator]; @@ -280,27 +283,36 @@ MySQL.prototype.toDatabase = function (prop, val) { return this.toDatabase(prop, val[0]) + ' AND ' + this.toDatabase(prop, val[1]); - } else if (operator == 'inq' || operator == 'nin') { - if (!(val.propertyIsEnumerable('length')) && typeof val === 'object' && typeof val.length === 'number') { //if value is array + } else if (operator === 'inq' || operator === 'nin') { + if (Array.isArray(val)) { //if value is array for (var i = 0; i < val.length; i++) { - val[i] = this.client.escape(val[i]); + val[i] = this.toDatabase(prop, val[i]); } return val.join(','); } else { - return val; + return this.toDatabase(prop, val); } } } - if (!prop) return val; - if (prop.type.name === 'Number') return Number(val); - if (prop.type.name === 'Date') { - if (!val) return 'NULL'; + if (!prop) { + return this.client.escape(val); + } + if (prop.type === Number) { + val = Number(val); + return isNaN(val) ? 'NULL' : val; + } + if (prop.type === Date) { + if (!val) { + return 'NULL'; + } if (!val.toUTCString) { val = new Date(val); } return '"' + dateToMysql(val) + '"'; } - if (prop.type.name == "Boolean") return val ? 1 : 0; + if (prop.type === Boolean) { + return val ? 1 : 0; + } if (prop.type.name === 'GeoPoint') { return val ? 'Point(' + val.lat + ',' + val.lng + ')' : 'NULL'; } @@ -399,6 +411,9 @@ MySQL.prototype.buildWhere = function (model, conds) { }; MySQL.prototype._buildWhere = function (model, conds) { + if (conds === null || conds === undefined || (typeof conds !== 'object')) { + return ''; + } var self = this; var props = self._models[model].properties; @@ -483,7 +498,13 @@ function buildOrderBy(self, model, order) { } function buildLimit(limit, offset) { - return 'LIMIT ' + (offset ? (offset + ', ' + limit) : limit); + if (isNaN(limit)) { + limit = 0; + } + if (isNaN(offset)) { + offset = 0; + } + return 'LIMIT ' + (offset ? (offset + ',' + limit) : limit); } /** @@ -517,7 +538,7 @@ MySQL.prototype.all = function all(model, filter, callback) { } if (filter.limit) { - sql += ' ' + buildLimit(filter.limit, filter.skip || 0); + sql += ' ' + buildLimit(filter.limit, filter.skip || filter.offset || 0); } } @@ -541,6 +562,21 @@ MySQL.prototype.all = function all(model, filter, callback) { }; +MySQL.prototype.count = function count(model, callback, where) { + + this.query('SELECT count(*) as cnt FROM ' + + this.tableEscaped(model) + ' ' + this.buildWhere(model, where), + function (err, res) { + if (err) { + return callback(err); + } + var c = (res && res[0] && res[0].cnt) || 0; + callback(err, c); + }); + +}; + + /** * Delete instances for the given model * @@ -554,7 +590,7 @@ MySQL.prototype.destroyAll = function destroyAll(model, where, callback) { callback = where; where = undefined; } - this.query('DELETE FROM ' + this.query('DELETE FROM ' + this.tableEscaped(model) + ' ' + this.buildWhere(model, where || {}), function (err, data) { callback && callback(err, data); diff --git a/package.json b/package.json index df3e715..0d3d579 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback-connector-mysql", - "version": "1.2.2", + "version": "1.2.3", "description": "MySQL connector for loopback-datasource-juggler", "main": "index.js", "scripts": { @@ -15,7 +15,7 @@ "loopback-datasource-juggler": "1.x.x", "should": "~1.3.0", "mocha": "~1.18.0", - "rc": "~0.3.1" + "rc": "~0.4.0" }, "repository": { "type": "git", diff --git a/test/mysql.test.js b/test/mysql.test.js new file mode 100644 index 0000000..e81f97b --- /dev/null +++ b/test/mysql.test.js @@ -0,0 +1,434 @@ +var should = require('./init.js'); + +var Post, PostWithStringId, db; + +describe('mysql', function () { + + before(function (done) { + db = getDataSource(); + + Post = db.define('PostWithDefaultId', { + title: { type: String, length: 255, index: true }, + content: { type: String }, + stars: Number + }); + + PostWithStringId = db.define('PostWithStringId', { + id: {type: String, id: true}, + title: { type: String, length: 255, index: true }, + content: { type: String } + }); + + db.automigrate(['PostWithDefaultId', 'PostWithStringId'], function (err) { + should.not.exist(err); + done(err); + }); + }); + + beforeEach(function (done) { + Post.destroyAll(function () { + PostWithStringId.destroyAll(function () { + done(); + }); + }); + }); + + it('updateOrCreate should update the instance', function (done) { + Post.create({title: 'a', content: 'AAA'}, function (err, post) { + post.title = 'b'; + Post.updateOrCreate(post, function (err, p) { + should.not.exist(err); + p.id.should.be.equal(post.id); + p.content.should.be.equal(post.content); + + Post.findById(post.id, function (err, p) { + p.id.should.be.equal(post.id); + + p.content.should.be.equal(post.content); + p.title.should.be.equal('b'); + + done(); + }); + }); + + }); + }); + + it('updateOrCreate should update the instance without removing existing properties', function (done) { + Post.create({title: 'a', content: 'AAA'}, function (err, post) { + post = post.toObject(); + delete post.title; + Post.updateOrCreate(post, function (err, p) { + should.not.exist(err); + p.id.should.be.equal(post.id); + p.content.should.be.equal(post.content); + Post.findById(post.id, function (err, p) { + p.id.should.be.equal(post.id); + + p.content.should.be.equal(post.content); + p.title.should.be.equal('a'); + + done(); + }); + }); + + }); + }); + + it('updateOrCreate should create a new instance if it does not exist', function (done) { + var post = {id: 123, title: 'a', content: 'AAA'}; + Post.updateOrCreate(post, function (err, p) { + should.not.exist(err); + p.title.should.be.equal(post.title); + p.content.should.be.equal(post.content); + p.id.should.be.equal(post.id); + + Post.findById(p.id, function (err, p) { + p.id.should.be.equal(post.id); + + p.content.should.be.equal(post.content); + p.title.should.be.equal(post.title); + p.id.should.be.equal(post.id); + + done(); + }); + }); + + }); + + it('save should update the instance with the same id', function (done) { + Post.create({title: 'a', content: 'AAA'}, function (err, post) { + post.title = 'b'; + post.save(function (err, p) { + should.not.exist(err); + p.id.should.be.equal(post.id); + p.content.should.be.equal(post.content); + + Post.findById(post.id, function (err, p) { + p.id.should.be.equal(post.id); + + p.content.should.be.equal(post.content); + p.title.should.be.equal('b'); + + done(); + }); + }); + + }); + }); + + it('save should update the instance without removing existing properties', function (done) { + Post.create({title: 'a', content: 'AAA'}, function (err, post) { + delete post.title; + post.save(function (err, p) { + should.not.exist(err); + p.id.should.be.equal(post.id); + p.content.should.be.equal(post.content); + + Post.findById(post.id, function (err, p) { + p.id.should.be.equal(post.id); + + p.content.should.be.equal(post.content); + p.title.should.be.equal('a'); + + done(); + }); + }); + + }); + }); + + it('save should create a new instance if it does not exist', function (done) { + var post = new Post({id: 123, title: 'a', content: 'AAA'}); + post.save(post, function (err, p) { + should.not.exist(err); + p.title.should.be.equal(post.title); + p.content.should.be.equal(post.content); + p.id.should.be.equal(post.id); + + Post.findById(p.id, function (err, p) { + should.not.exist(err); + p.id.should.be.equal(post.id); + + p.content.should.be.equal(post.content); + p.title.should.be.equal(post.title); + p.id.should.be.equal(post.id); + + done(); + }); + }); + + }); + + it('all return should honor filter.fields', function (done) { + var post = new Post({title: 'b', content: 'BBB'}) + post.save(function (err, post) { + Post.all({fields: ['title'], where: {title: 'b'}}, function (err, posts) { + should.not.exist(err); + posts.should.have.lengthOf(1); + post = posts[0]; + post.should.have.property('title', 'b'); + post.should.not.have.property('content'); + should.not.exist(post.id); + + done(); + }); + + }); + }); + + it('find should order by id if the order is not set for the query filter', + function (done) { + PostWithStringId.create({id: '2', title: 'c', content: 'CCC'}, function (err, post) { + PostWithStringId.create({id: '1', title: 'd', content: 'DDD'}, function (err, post) { + PostWithStringId.find(function (err, posts) { + should.not.exist(err); + posts.length.should.be.equal(2); + posts[0].id.should.be.equal('1'); + + PostWithStringId.find({limit: 1, offset: 0}, function (err, posts) { + should.not.exist(err); + posts.length.should.be.equal(1); + posts[0].id.should.be.equal('1'); + + PostWithStringId.find({limit: 1, offset: 1}, function (err, posts) { + should.not.exist(err); + posts.length.should.be.equal(1); + posts[0].id.should.be.equal('2'); + done(); + }); + }); + }); + }); + }); + }); + + it('should allow to find using like', function (done) { + Post.create({title: 'My Post', content: 'Hello'}, function (err, post) { + Post.find({where: {title: {like: 'M%st'}}}, function (err, posts) { + should.not.exist(err); + posts.should.have.property('length', 1); + done(); + }); + }); + }); + + it('should support like for no match', function (done) { + Post.create({title: 'My Post', content: 'Hello'}, function (err, post) { + Post.find({where: {title: {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) { + Post.create({title: 'My Post', content: 'Hello'}, function (err, post) { + Post.find({where: {title: {nlike: 'M%st'}}}, function (err, posts) { + should.not.exist(err); + posts.should.have.property('length', 0); + done(); + }); + }); + }); + + it('should support nlike for no match', function (done) { + Post.create({title: 'My Post', content: 'Hello'}, function (err, post) { + Post.find({where: {title: {nlike: 'M%XY'}}}, function (err, posts) { + should.not.exist(err); + posts.should.have.property('length', 1); + done(); + }); + }); + }); + + it('should support "and" operator that is satisfied', function (done) { + Post.create({title: 'My Post', content: 'Hello'}, function (err, post) { + Post.find({where: {and: [ + {title: 'My Post'}, + {content: 'Hello'} + ]}}, function (err, posts) { + should.not.exist(err); + posts.should.have.property('length', 1); + done(); + }); + }); + }); + + it('should support "and" operator that is not satisfied', function (done) { + Post.create({title: 'My Post', content: 'Hello'}, function (err, post) { + Post.find({where: {and: [ + {title: 'My Post'}, + {content: 'Hello1'} + ]}}, function (err, posts) { + should.not.exist(err); + posts.should.have.property('length', 0); + done(); + }); + }); + }); + + it('should support "or" that is satisfied', function (done) { + Post.create({title: 'My Post', content: 'Hello'}, function (err, post) { + Post.find({where: {or: [ + {title: 'My Post'}, + {content: 'Hello1'} + ]}}, function (err, posts) { + should.not.exist(err); + posts.should.have.property('length', 1); + done(); + }); + }); + }); + + it('should support "or" operator that is not satisfied', function (done) { + Post.create({title: 'My Post', content: 'Hello'}, function (err, post) { + Post.find({where: {or: [ + {title: 'My Post1'}, + {content: 'Hello1'} + ]}}, function (err, posts) { + should.not.exist(err); + posts.should.have.property('length', 0); + done(); + }); + }); + }); + + // The where object should be parsed by the connector + it('should support where for count', function (done) { + Post.create({title: 'My Post', content: 'Hello'}, function (err, post) { + Post.count({and: [ + {title: 'My Post'}, + {content: 'Hello'} + ]}, function (err, count) { + should.not.exist(err); + count.should.be.equal(1); + Post.count({and: [ + {title: 'My Post1'}, + {content: 'Hello'} + ]}, function (err, count) { + should.not.exist(err); + count.should.be.equal(0); + done(); + }); + }); + }); + }); + + // The where object should be parsed by the connector + it('should support where for destroyAll', function (done) { + Post.create({title: 'My Post1', content: 'Hello'}, function (err, post) { + Post.create({title: 'My Post2', content: 'Hello'}, function (err, post) { + Post.destroyAll({and: [ + {title: 'My Post1'}, + {content: 'Hello'} + ]}, function (err) { + should.not.exist(err); + Post.count(function (err, count) { + should.not.exist(err); + count.should.be.equal(1); + done(); + }); + }); + }); + }); + }); + + it('should not allow SQL injection for inq operator', function (done) { + Post.create({title: 'My Post1', content: 'Hello', stars: 5}, + function (err, post) { + Post.create({title: 'My Post2', content: 'Hello', stars: 20}, + function (err, post) { + Post.find({where: {title: {inq: 'SELECT title from PostWithDefaultId'}}}, + function (err, posts) { + should.not.exist(err); + posts.should.have.property('length', 0); + done(); + }); + }); + }); + }); + + it('should not allow SQL injection for lt operator', function (done) { + Post.create({title: 'My Post1', content: 'Hello', stars: 5}, + function (err, post) { + Post.create({title: 'My Post2', content: 'Hello', stars: 20}, + function (err, post) { + Post.find({where: {stars: {lt: 'SELECT title from PostWithDefaultId'}}}, + function (err, posts) { + should.not.exist(err); + posts.should.have.property('length', 0); + done(); + }); + }); + }); + }); + + it('should not allow SQL injection for nin operator', function (done) { + Post.create({title: 'My Post1', content: 'Hello', stars: 5}, + function (err, post) { + Post.create({title: 'My Post2', content: 'Hello', stars: 20}, + function (err, post) { + Post.find({where: {title: {nin: 'SELECT title from PostWithDefaultId'}}}, + function (err, posts) { + should.not.exist(err); + posts.should.have.property('length', 2); + done(); + }); + }); + }); + }); + + + it('should not allow SQL injection for inq operator with number column', function (done) { + Post.create({title: 'My Post1', content: 'Hello', stars: 5}, + function (err, post) { + Post.create({title: 'My Post2', content: 'Hello', stars: 20}, + function (err, post) { + Post.find({where: {stars: {inq: 'SELECT title from PostWithDefaultId'}}}, + function (err, posts) { + should.not.exist(err); + posts.should.have.property('length', 0); + done(); + }); + }); + }); + }); + + it('should not allow SQL injection for inq operator with array value', function (done) { + Post.create({title: 'My Post1', content: 'Hello', stars: 5}, + function (err, post) { + Post.create({title: 'My Post2', content: 'Hello', stars: 20}, + function (err, post) { + Post.find({where: {stars: {inq: [5, 'SELECT title from PostWithDefaultId']}}}, + function (err, posts) { + should.not.exist(err); + posts.should.have.property('length', 1); + done(); + }); + }); + }); + }); + + it('should not allow SQL injection for between operator', function (done) { + Post.create({title: 'My Post1', content: 'Hello', stars: 5}, + function (err, post) { + Post.create({title: 'My Post2', content: 'Hello', stars: 20}, + function (err, post) { + Post.find({where: {stars: {between: [5, 'SELECT title from PostWithDefaultId']}}}, + function (err, posts) { + should.not.exist(err); + posts.should.have.property('length', 0); + done(); + }); + }); + }); + }); + + after(function (done) { + Post.destroyAll(function () { + PostWithStringId.destroyAll(done); + }); + }); +});