// Copyright IBM Corp. 2013,2019. All Rights Reserved. // Node module: loopback-connector-mysql // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT 'use strict'; const async = require('async'); const should = require('./init.js'); const sinon = require('sinon'); const List = require('loopback-datasource-juggler/lib/list'); let Post, PostWithStringId, PostWithUniqueTitle, PostWithNumId, Student, db; // Mock up mongodb ObjectID function ObjectID(id) { if (!(this instanceof ObjectID)) { return new ObjectID(id); } this.id1 = id.substring(0, 2); this.id2 = id.substring(2); } ObjectID.prototype.toJSON = function() { return this.id1 + this.id2; }; describe('mysql', function() { before(function(done) { db = global.getDataSource(); Post = db.define('PostWithDefaultId', { title: {type: String, length: 255, index: true}, content: {type: String}, comments: [String], history: Object, stars: Number, userId: ObjectID, }, { forceId: false, }); PostWithStringId = db.define('PostWithStringId', { id: {type: String, id: true}, title: {type: String, length: 255, index: true}, content: {type: String}, }); PostWithNumId = db.define('PostWithNumId', { id: {type: Number, id: true, null: false}, title: {type: String, length: 255, null: false, index: true}, content: {type: String, null: false}, buffProp: {type: Buffer, null: false}, objProp: {type: Object, null: false}, arrProp: {type: [Number], null: false}, dateProp: {type: Date, null: false}, pointProp: {type: 'GeoPoint', null: false}, }); PostWithUniqueTitle = db.define('PostWithUniqueTitle', { title: {type: String, length: 255, index: {unique: true}}, content: {type: String}, }); Student = db.define('Student', { name: {type: String, length: 255}, age: {type: Number}, }, { forceId: false, }); db.automigrate( ['PostWithDefaultId', 'PostWithStringId', 'PostWithUniqueTitle', 'PostWithNumId', 'Student'], function(err) { should.not.exist(err); done(err); }, ); }); beforeEach(function() { return deleteAllModelInstances(); }); it('should allow array or object', function(done) { Post.create({title: 'a', content: 'AAA', comments: ['1', '2'], history: {a: 1, b: 'b'}}, function(err, post) { should.not.exist(err); 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'); p.comments.should.eql(new List(['1', '2'])); p.history.should.eql({a: 1, b: 'b'}); done(); }); }); }); it('should allow ObjectID', function(done) { const uid = new ObjectID('123'); Post.create({title: 'a', content: 'AAA', userId: uid}, function(err, post) { should.not.exist(err); 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'); p.userId.should.eql(uid); 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) { const 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(); }); }); }); context('replaceOrCreate', function() { it('should replace the instance', function(done) { Post.create({title: 'a', content: 'AAA'}, function(err, post) { if (err) return done(err); post = post.toObject(); delete post.content; Post.replaceOrCreate(post, function(err, p) { if (err) return done(err); p.id.should.equal(post.id); p.title.should.equal('a'); should.not.exist(p.content); should.not.exist(p._id); Post.findById(post.id, function(err, p) { if (err) return done(err); p.id.should.equal(post.id); p.title.should.equal('a'); should.not.exist(post.content); should.not.exist(p._id); done(); }); }); }); }); it('should replace with new data', function(done) { Post.create({title: 'a', content: 'AAA', comments: ['Comment1']}, function(err, post) { if (err) return done(err); post = post.toObject(); delete post.comments; delete post.content; post.title = 'b'; Post.replaceOrCreate(post, function(err, p) { if (err) return done(err); p.id.should.equal(post.id); should.not.exist(p._id); p.title.should.equal('b'); should.not.exist(p.content); should.not.exist(p.comments); Post.findById(post.id, function(err, p) { if (err) return done(err); p.id.should.equal(post.id); should.not.exist(p._id); p.title.should.equal('b'); should.not.exist(p.content); should.not.exist(p.comments); done(); }); }); }); }); it('should create a new instance if it does not exist', function(done) { const post = {id: 123, title: 'a', content: 'AAA'}; Post.replaceOrCreate(post, function(err, p) { if (err) return done(err); p.id.should.equal(post.id); should.not.exist(p._id); p.title.should.equal(post.title); p.content.should.equal(post.content); Post.findById(p.id, function(err, p) { if (err) return done(err); p.id.should.equal(post.id); should.not.exist(p._id); p.title.should.equal(post.title); p.content.should.equal(post.content); done(); }); }); }); it('isNewInstance should be undefined for after save hook', function(done) { const student = {name: 'Joe', age: 20}; const newStudent = {}; let isNewInstanceBefore = false; let isNewInstanceAfter = false; Student.create(student, function(err, createdStudent) { if (err) return done(err); newStudent.id = createdStudent.id; newStudent.name = 'Hannah'; newStudent.age = 25; Student.observe('before save', function(ctx, next) { isNewInstanceBefore = ctx.isNewInstance; next(); }); Student.observe('after save', function(ctx, next) { isNewInstanceAfter = ctx.isNewInstance; next(); }); Student.replaceOrCreate(newStudent, function(err, s) { if (err) return done(err); should.not.exist(isNewInstanceBefore); should.not.exist(isNewInstanceAfter); 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) { const 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) { const 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.have.property('content', undefined); 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(); }); }); }); context('null vals in different operators', function() { const defaultPost = { id: 3, title: 'defTitle', content: 'defContent', buffProp: new Buffer('defBuffer'), objProp: {defKey: 'defVal'}, arrProp: [0], dateProp: new Date('2017-06-14'), pointProp: {lng: 4.51515, lat: 57.2314}, }; beforeEach(function(done) { PostWithNumId.destroyAll(done); }); after(function(done) { PostWithNumId.destroyAll(done); }); it('should handle null in inq operator', function(done) { defaultPost.id = 1; defaultPost.title = 'Foo'; defaultPost.content = 'Bar'; PostWithNumId.create(defaultPost, function(err, post) { should.not.exist(err); post.id.should.equal(defaultPost.id); PostWithNumId.find({where: {id: {inq: [null, 1]}}}, function(err, posts) { should.not.exist(err); posts.length.should.equal(1); posts[0].title.should.equal('Foo'); posts[0].id.should.equal(1); done(); }); }); }); it('should handle null in nin operator', function(done) { defaultPost.id = 2; defaultPost.title = 'Make'; defaultPost.content = 'Toyota'; PostWithNumId.create(defaultPost, function(err, post) { should.not.exist(err); post.id.should.equal(defaultPost.id); PostWithNumId.find({where: {id: {nin: [null, 3]}}}, function(err, posts) { should.not.exist(err); posts.length.should.equal(1); posts[0].content.should.equal('Toyota'); posts[0].id.should.equal(2); done(); }); }); }); it('should handle null in neq operator', function(done) { defaultPost.id = 3; defaultPost.title = 'Model'; defaultPost.content = 'Corolla'; PostWithNumId.create(defaultPost, function(err, post) { should.not.exist(err); post.id.should.equal(defaultPost.id); PostWithNumId.find({where: {id: {neq: null}}}, function(err, posts) { should.not.exist(err); posts.length.should.equal(1); posts[0].content.should.equal('Corolla'); posts[0].id.should.equal(3); done(); }); }); }); it('should handle null in nin op for different datatypes', function(done) { PostWithNumId.create(defaultPost, function(err, post) { should.not.exist(err); post.id.should.equal(defaultPost.id); PostWithNumId.find({where: {and: [ {id: {nin: [null]}}, {title: {nin: [null]}}, {content: {nin: [null]}}, {buffProp: {nin: [null]}}, {objProp: {nin: [null]}}, {arrProp: {nin: [null]}}, {dateProp: {nin: [null]}}, {pointProp: {nin: [null]}}, ]}}, function(err, posts) { should.not.exist(err); posts.length.should.equal(1); posts[0].toObject().should.deepEqual(defaultPost); 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(); }); }); }); }); it('should not allow duplicate titles', function(done) { const data = {title: 'a', content: 'AAA'}; PostWithUniqueTitle.create(data, function(err, post) { should.not.exist(err); PostWithUniqueTitle.create(data, function(err, post) { should.exist(err); done(); }); }); }); context('regexp operator', function() { beforeEach(function deleteExistingTestFixtures(done) { Post.destroyAll(done); }); beforeEach(function createTestFixtures(done) { Post.create([ {title: 'a', content: 'AAA'}, {title: 'b', content: 'BBB'}, ], done); }); after(function deleteTestFixtures(done) { Post.destroyAll(done); }); context('with regex strings', function() { context('using no flags', function() { it('should work', function(done) { Post.find({where: {content: {regexp: '^A'}}}, function(err, posts) { should.not.exist(err); posts.length.should.equal(1); posts[0].content.should.equal('AAA'); done(); }); }); }); context('using flags', function() { beforeEach(function addSpy() { sinon.stub(console, 'warn'); }); afterEach(function removeSpy() { console.warn.restore(); }); it('should work', function(done) { Post.find({where: {content: {regexp: '^a/i'}}}, function(err, posts) { should.not.exist(err); posts.length.should.equal(1); posts[0].content.should.equal('AAA'); done(); }); }); it('should print a warning when the ignore flag is set', function(done) { Post.find({where: {content: {regexp: '^a/i'}}}, function(err, posts) { console.warn.calledOnce.should.be.ok; done(); }); }); it('should print a warning when the global flag is set', function(done) { Post.find({where: {content: {regexp: '^a/g'}}}, function(err, posts) { console.warn.calledOnce.should.be.ok; done(); }); }); it('should print a warning when the multiline flag is set', function(done) { Post.find({where: {content: {regexp: '^a/m'}}}, function(err, posts) { console.warn.calledOnce.should.be.ok; done(); }); }); }); it('filter with case sensitive regex string', function(done) { Post.find({where: {content: {regexp: '^a'}}}, function(err, posts) { should.not.exist(err); should.exist(posts); posts.length.should.equal(0); done(); }); }); }); context('with regex literals', function() { context('using no flags', function() { it('should work', function(done) { Post.find({where: {content: {regexp: /^A/}}}, function(err, posts) { should.not.exist(err); posts.length.should.equal(1); posts[0].content.should.equal('AAA'); done(); }); }); }); context('using flags', function() { beforeEach(function addSpy() { sinon.stub(console, 'warn'); }); afterEach(function removeSpy() { console.warn.restore(); }); it('should work', function(done) { Post.find({where: {content: {regexp: /^a/i}}}, function(err, posts) { should.not.exist(err); posts.length.should.equal(1); posts[0].content.should.equal('AAA'); done(); }); }); it('should print a warning when the ignore flag is set', function(done) { Post.find({where: {content: {regexp: /^a/i}}}, function(err, posts) { console.warn.calledOnce.should.be.ok; done(); }); }); it('should print a warning when the global flag is set', function(done) { Post.find({where: {content: {regexp: /^a/g}}}, function(err, posts) { console.warn.calledOnce.should.be.ok; done(); }); }); it('should print a warning when the multiline flag is set', function(done) { Post.find({where: {content: {regexp: /^a/m}}}, function(err, posts) { console.warn.calledOnce.should.be.ok; done(); }); }); }); it('filter with case sensitive regex literal', function(done) { Post.find({where: {content: {regexp: /^B/}}}, function(err, posts) { should.not.exist(err); should.exist(posts); posts.length.should.equal(1); posts[0].content.should.equal('BBB'); done(); }); }); }); context('with regex objects', function() { beforeEach(function addSpy() { sinon.stub(console, 'warn'); }); afterEach(function removeSpy() { console.warn.restore(); }); context('using no flags', function() { it('should work', function(done) { Post.find({where: {content: {regexp: new RegExp(/^A/)}}}, function(err, posts) { should.not.exist(err); posts.length.should.equal(1); posts[0].content.should.equal('AAA'); done(); }); }); }); context('using flags', function() { it('should work', function(done) { Post.find({where: {content: {regexp: new RegExp(/^a/i)}}}, function(err, posts) { should.not.exist(err); posts.length.should.equal(1); posts[0].content.should.equal('AAA'); done(); }); }); it('should print a warning when the ignore flag is set', function(done) { Post.find({where: {content: {regexp: new RegExp(/^a/i)}}}, function(err, posts) { console.warn.calledOnce.should.be.ok; done(); }); }); it('should print a warning when the global flag is set', function(done) { Post.find({where: {content: {regexp: new RegExp(/^a/g)}}}, function(err, posts) { console.warn.calledOnce.should.be.ok; done(); }); }); it('should print a warning when the multiline flag is set', function(done) { Post.find({where: {content: {regexp: new RegExp(/^a/m)}}}, function(err, posts) { console.warn.calledOnce.should.be.ok; done(); }); }); it('filter with case sensitive regex object', function(done) { Post.find({where: {content: {regexp: new RegExp(/^a/)}}}, function(err, posts) { should.not.exist(err); should.exist(posts); posts.length.should.equal(0); done(); }); }); }); }); }); function deleteAllModelInstances() { const models = [ Post, PostWithStringId, PostWithUniqueTitle, PostWithNumId, Student, ]; return Promise.all(models.map(m => m.destroyAll())); } after(function() { return deleteAllModelInstances(); }); });