Merge branch 'release/1.2.3' into production

This commit is contained in:
Raymond Feng 2014-06-02 11:33:39 -07:00
commit 39445698e9
3 changed files with 488 additions and 18 deletions

View File

@ -203,7 +203,7 @@ MySQL.prototype.create = function (model, data, callback) {
* @param {Object} data The model instance data * @param {Object} data The model instance data
* @param {Function} [callback] The callback function * @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 mysql = this;
var fieldsNames = []; var fieldsNames = [];
var fieldValues = []; var fieldValues = [];
@ -244,7 +244,9 @@ MySQL.prototype.toFields = function (model, data) {
Object.keys(data).forEach(function (key) { Object.keys(data).forEach(function (key) {
if (props[key]) { if (props[key]) {
var value = this.toDatabase(props[key], data[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); fields.push(self.columnEscaped(model, key) + ' = ' + value);
} }
}.bind(this)); }.bind(this));
@ -271,8 +273,9 @@ function dateToMysql(val) {
* @returns {*} * @returns {*}
*/ */
MySQL.prototype.toDatabase = function (prop, val) { MySQL.prototype.toDatabase = function (prop, val) {
if (val === null) return 'NULL'; if (val === null || val === undefined) {
if (val === undefined) return 'NULL'; return 'NULL';
}
if (val.constructor.name === 'Object') { if (val.constructor.name === 'Object') {
var operator = Object.keys(val)[0] var operator = Object.keys(val)[0]
val = val[operator]; val = val[operator];
@ -280,27 +283,36 @@ MySQL.prototype.toDatabase = function (prop, val) {
return this.toDatabase(prop, val[0]) + return this.toDatabase(prop, val[0]) +
' AND ' + ' AND ' +
this.toDatabase(prop, val[1]); this.toDatabase(prop, val[1]);
} else if (operator == 'inq' || operator == 'nin') { } else if (operator === 'inq' || operator === 'nin') {
if (!(val.propertyIsEnumerable('length')) && typeof val === 'object' && typeof val.length === 'number') { //if value is array if (Array.isArray(val)) { //if value is array
for (var i = 0; i < val.length; i++) { 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(','); return val.join(',');
} else { } else {
return val; return this.toDatabase(prop, val);
} }
} }
} }
if (!prop) return val; if (!prop) {
if (prop.type.name === 'Number') return Number(val); return this.client.escape(val);
if (prop.type.name === 'Date') { }
if (!val) return 'NULL'; if (prop.type === Number) {
val = Number(val);
return isNaN(val) ? 'NULL' : val;
}
if (prop.type === Date) {
if (!val) {
return 'NULL';
}
if (!val.toUTCString) { if (!val.toUTCString) {
val = new Date(val); val = new Date(val);
} }
return '"' + dateToMysql(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') { if (prop.type.name === 'GeoPoint') {
return val ? 'Point(' + val.lat + ',' + val.lng + ')' : 'NULL'; return val ? 'Point(' + val.lat + ',' + val.lng + ')' : 'NULL';
} }
@ -399,6 +411,9 @@ MySQL.prototype.buildWhere = function (model, conds) {
}; };
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 self = this;
var props = self._models[model].properties; var props = self._models[model].properties;
@ -483,7 +498,13 @@ function buildOrderBy(self, model, order) {
} }
function buildLimit(limit, offset) { 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) { 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 * Delete instances for the given model
* *

View File

@ -1,6 +1,6 @@
{ {
"name": "loopback-connector-mysql", "name": "loopback-connector-mysql",
"version": "1.2.2", "version": "1.2.3",
"description": "MySQL connector for loopback-datasource-juggler", "description": "MySQL connector for loopback-datasource-juggler",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
@ -15,7 +15,7 @@
"loopback-datasource-juggler": "1.x.x", "loopback-datasource-juggler": "1.x.x",
"should": "~1.3.0", "should": "~1.3.0",
"mocha": "~1.18.0", "mocha": "~1.18.0",
"rc": "~0.3.1" "rc": "~0.4.0"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

434
test/mysql.test.js Normal file
View File

@ -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);
});
});
});