diff --git a/docs.json b/docs.json index 2ada8c2..dd74bbf 100644 --- a/docs.json +++ b/docs.json @@ -1,8 +1,8 @@ { "content": [ - "README.md", {"title": "LoopBack MySQL Connector API", "depth": 2}, "lib/mysql.js", + {"title": "MySQL Discovery API", "depth": 2}, "lib/discovery.js" ], "codeSectionDepth": 3 diff --git a/lib/mysql.js b/lib/mysql.js index 02a3b0e..3400e53 100644 --- a/lib/mysql.js +++ b/lib/mysql.js @@ -20,18 +20,24 @@ exports.initialize = function initializeDataSource(dataSource, callback) { } var s = dataSource.settings; - + if (s.collation) { - s.charset = s.collation.substr(0,s.collation.indexOf('_')); // Charset should be first 'chunk' of collation. + s.charset = s.collation.substr(0, s.collation.indexOf('_')); // Charset should be first 'chunk' of collation. } else { s.collation = 'utf8_general_ci'; s.charset = 'utf8'; } - + s.supportBigNumbers = (s.supportBigNumbers || false); s.timezone = (s.timezone || 'local'); - - dataSource.client = mysql.createConnection({ + + if(isNaN(s.connectionLimit)) { + s.connectionLimit = s.connectionLimit; + } else { + s.connectionLimit = 10; + } + + var options = { host: s.host || s.hostname || 'localhost', port: s.port || 3306, user: s.username || s.user, @@ -40,44 +46,39 @@ exports.initialize = function initializeDataSource(dataSource, callback) { debug: s.debug, socketPath: s.socketPath, charset: s.collation.toUpperCase(), // Correct by docs despite seeming odd. - supportBigNumbers: s.supportBigNumbers - }); + supportBigNumbers: s.supportBigNumbers, + connectionLimit: s.connectionLimit + }; + + // Don't configure the DB if the pool can be used for multiple DBs + if(!s.createDatabase) { + options.database = s.database; + } + + dataSource.client = mysql.createPool(options); dataSource.client.on('error', function (err) { dataSource.emit('error', err); + dataSource.connected = false; + dataSource.connecting = false; }); - if(s.debug) { + if (s.debug) { console.log('Settings: ', s); } dataSource.connector = new MySQL(dataSource.client, s); dataSource.connector.dataSource = dataSource; - dataSource.client.query('USE `' + s.database + '`', function (err) { - if (err) { - if (err.message.match(/(^|: )unknown database/i)) { - var dbName = s.database; - var charset = s.charset; - var collation = s.collation; - var q = 'CREATE DATABASE ' + dbName + ' CHARACTER SET ' + charset + ' COLLATE ' + collation; - dataSource.client.query(q, function (error) { - if (!error) { - dataSource.client.query('USE ' + s.database, callback); - } else { - throw error; - } - }); - } else throw err; - } else callback(); + // MySQL specific column types + juggler.ModelBuilder.registerType(function Point() { }); - // MySQL specific column types - juggler.ModelBuilder.registerType(function Point() {}); - dataSource.EnumFactory = EnumFactory; // factory for Enums. Note that currently Enums can not be registered. - - + + process.nextTick(function() { + callback && callback(); + }); }; exports.MySQL = MySQL; @@ -103,6 +104,7 @@ require('util').inherits(MySQL, juggler.BaseSQL); * @param {Function} [callback] The callback after the SQL statement is executed */ MySQL.prototype.query = function (sql, callback) { + var self = this; if (!this.dataSource.connected) { return this.dataSource.on('connected', function () { this.query(sql, callback); @@ -111,36 +113,69 @@ MySQL.prototype.query = function (sql, callback) { var client = this.client; var time = Date.now(); var debug = this.settings.debug; + var db = this.settings.database; var log = this.log; if (typeof callback !== 'function') throw new Error('callback should be a function'); - if(debug) { - console.log('SQL:' , sql); + if (debug) { + console.log('SQL:', sql); } - this.client.query(sql, function (err, data) { - if(err) { - if(debug) { - console.error('Error:', err); - } - } - if (err && err.message.match(/(^|: )unknown database/i)) { - var dbName = err.message.match(/(^|: )unknown database '(.*?)'/i)[1]; - client.query('CREATE DATABASE ' + dbName, function (error) { - if (!error) { - client.query(sql, callback); - } else { - callback(err); + + function releaseConnectionAndCallback(connection, err, result) { + connection.release(); + callback && callback(err, result); + } + + function runQuery(connection) { + connection.query(sql, function (err, data) { + if (debug) { + if (err) { + console.error('Error:', err); } - }); + console.log('Data:', data); + } + if (log) log(sql, time); + releaseConnectionAndCallback(connection, err, data); + }); + } + + client.getConnection(function (err, connection) { + if (err) { + callback && callback(err); return; } - if(debug) { - console.log('Data:' , data); + if (self.settings.createDatabase) { + // Call USE db ... + connection.query('USE `' + db + '`', function (err) { + if (err) { + if (err && err.message.match(/(^|: )unknown database/i)) { + var charset = self.settings.charset; + var collation = self.settings.collation; + var q = 'CREATE DATABASE ' + db + ' CHARACTER SET ' + charset + ' COLLATE ' + collation; + connection.query(q, function (err) { + if (!err) { + connection.query('USE `' + db + '`', function (err) { + runQuery(connection); + }); + } else { + releaseConnectionAndCallback(connection, err); + } + }); + return; + } else { + releaseConnectionAndCallback(connection, err); + return; + } + } + runQuery(connection); + }); + } else { + // Bypass USE db + runQuery(connection); } - if (log) log(sql, time); - callback(err, data); }); }; + /** * Create the data model in MySQL * @@ -243,8 +278,8 @@ MySQL.prototype.toDatabase = function (prop, val) { val = val[operator]; if (operator === 'between') { return this.toDatabase(prop, val[0]) + - ' AND ' + - this.toDatabase(prop, val[1]); + ' 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 for (var i = 0; i < val.length; i++) { @@ -267,7 +302,7 @@ MySQL.prototype.toDatabase = function (prop, val) { } if (prop.type.name == "Boolean") return val ? 1 : 0; if (prop.type.name === 'GeoPoint') { - return val ? 'Point(' + val.lat + ',' + val.lng +')' : 'NULL'; + return val ? 'Point(' + val.lat + ',' + val.lng + ')' : 'NULL'; } if (typeof prop.type === 'function') return this.client.escape(prop.type(val)); return this.client.escape(val.toString()); @@ -285,27 +320,27 @@ MySQL.prototype.fromDatabase = function (model, data) { } var props = this._models[model].properties; var json = {}; - for(var p in props) { + for (var p in props) { var key = this.column(model, p); var val = data[key]; if (typeof val === 'undefined' || val === null) { continue; } if (props[p]) { - switch(props[p].type.name) { + switch (props[p].type.name) { case 'Date': - val = new Date(val.toString().replace(/GMT.*$/, 'GMT')); - break; + val = new Date(val.toString().replace(/GMT.*$/, 'GMT')); + break; case 'Boolean': - val = Boolean(val); - break; + val = Boolean(val); + break; case 'GeoPoint': case 'Point': - val = { - lat: val.x, - lng: val.y - }; - break; + val = { + lat: val.x, + lng: val.y + }; + break; } } json[p] = val; @@ -317,9 +352,9 @@ MySQL.prototype.escapeName = function (name) { return '`' + name.replace(/\./g, '`.`') + '`'; }; -MySQL.prototype.getColumns = function(model, props){ +MySQL.prototype.getColumns = function (model, props) { var cols = this._models[model].properties; - if(!cols) { + if (!cols) { return '*'; } var self = this; @@ -347,7 +382,7 @@ MySQL.prototype.getColumns = function(model, props){ }); } } - var names = keys.map(function(c) { + var names = keys.map(function (c) { return self.columnEscaped(model, c); }); return names.join(', '); @@ -411,7 +446,7 @@ function buildOrderBy(self, model, order) { if (typeof order === 'string') { order = [order]; } - return 'ORDER BY ' + order.map(function(o) { + return 'ORDER BY ' + order.map(function (o) { var t = o.split(/[\s,]+/); if (t.length === 1) { return self.columnEscaped(model, o); @@ -435,14 +470,14 @@ MySQL.prototype.all = function all(model, filter, callback) { var self = this; // Order by id if no order is specified filter = filter || {}; - if(!filter.order) { + if (!filter.order) { var idNames = this.idNames(model); - if(idNames && idNames.length) { + if (idNames && idNames.length) { filter.order = idNames.join(' '); } } - var sql = 'SELECT '+ this.getColumns(model, filter.fields) + ' FROM ' + this.tableEscaped(model); + var sql = 'SELECT ' + this.getColumns(model, filter.fields) + ' FROM ' + this.tableEscaped(model); if (filter) { @@ -488,7 +523,7 @@ MySQL.prototype.all = function all(model, filter, callback) { * */ MySQL.prototype.destroyAll = function destroyAll(model, where, callback) { - if(!callback && 'function' === typeof where) { + if (!callback && 'function' === typeof where) { callback = where; where = undefined; } @@ -552,7 +587,7 @@ MySQL.prototype.createTable = function (model, cb) { var engine = metadata && metadata.engine; var sql = 'CREATE TABLE ' + this.tableEscaped(model) + ' (\n ' + this.propertiesSQL(model) + '\n)'; - if(engine) { + if (engine) { sql += 'ENGINE=' + engine + '\n'; } this.query(sql, cb); @@ -645,7 +680,7 @@ MySQL.prototype.alterTable = function (model, actualFields, actualIndexes, done, // remove indexes aiNames.forEach(function (indexName) { - if (indexName === 'PRIMARY'|| (m.properties[indexName] && self.id(model, indexName))) return; + if (indexName === 'PRIMARY' || (m.properties[indexName] && self.id(model, indexName))) return; if (indexNames.indexOf(indexName) === -1 && !m.properties[indexName] || m.properties[indexName] && !m.properties[indexName].index) { sql.push('DROP INDEX `' + indexName + '`'); } else { @@ -733,15 +768,15 @@ MySQL.prototype.alterTable = function (model, actualFields, actualIndexes, done, function changed(newSettings, oldSettings) { if (oldSettings.Null === 'YES') { // Used to allow null and does not now. - if(newSettings.allowNull === false) return true; - if(newSettings.null === false) return true; + if (newSettings.allowNull === false) return true; + if (newSettings.null === false) return true; } if (oldSettings.Null === 'NO') { // Did not allow null and now does. - if(newSettings.allowNull === true) return true; - if(newSettings.null === true) return true; - if(newSettings.null === undefined && newSettings.allowNull === undefined) return true; + if (newSettings.allowNull === true) return true; + if (newSettings.null === true) return true; + if (newSettings.null === undefined && newSettings.allowNull === undefined) return true; } - + if (oldSettings.Type.toUpperCase() !== datatype(newSettings).toUpperCase()) return true; return false; } @@ -771,12 +806,12 @@ MySQL.prototype.propertiesSQL = function (model) { } /* - var sql = ['`id` INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY']; - Object.keys(this._models[model].properties).forEach(function (prop) { - if (self.id(model, prop)) return; - sql.push('`' + prop + '` ' + self.propertySettingsSQL(model, prop)); - }); - */ + var sql = ['`id` INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY']; + Object.keys(this._models[model].properties).forEach(function (prop) { + if (self.id(model, prop)) return; + sql.push('`' + prop + '` ' + self.propertySettingsSQL(model, prop)); + }); + */ // Declared in model index property indexes. Object.keys(this._models[model].properties).forEach(function (prop) { @@ -787,8 +822,8 @@ MySQL.prototype.propertiesSQL = function (model) { }); // Settings might not have an indexes property. var dxs = this._models[model].settings.indexes; - if(dxs){ - Object.keys(this._models[model].settings.indexes).forEach(function(prop){ + if (dxs) { + Object.keys(this._models[model].settings.indexes).forEach(function (prop) { sql.push(self.indexSettingsSQL(model, prop)); }); } @@ -835,19 +870,19 @@ MySQL.prototype.indexSettingsSQL = function (model, prop) { MySQL.prototype.propertySettingsSQL = function (model, prop) { var p = this._models[model].properties[prop]; - var line = this.columnDataType(model, prop) + ' ' + - (p.nullable === false || p.allowNull === false || p['null'] === false ? 'NOT NULL' : 'NULL'); + var line = this.columnDataType(model, prop) + ' ' + + (p.nullable === false || p.allowNull === false || p['null'] === false ? 'NOT NULL' : 'NULL'); return line; }; MySQL.prototype.columnDataType = function (model, property) { var columnMetadata = this.columnMetadata(model, property); var colType = columnMetadata && columnMetadata.dataType; - if(colType) { + if (colType) { colType = colType.toUpperCase(); } var prop = this._models[model].properties[property]; - if(!prop) { + if (!prop) { return null; } var colLength = columnMetadata && columnMetadata.dataLength || prop.length; @@ -865,36 +900,36 @@ function datatype(p) { case 'JSON': dt = columnType(p, 'VARCHAR'); dt = stringOptionsByType(p, dt); - break; + break; case 'Text': dt = columnType(p, 'TEXT'); dt = stringOptionsByType(p, dt); - break; + break; case 'Number': dt = columnType(p, 'INT'); dt = numericOptionsByType(p, dt); - break; + break; case 'Date': dt = columnType(p, 'DATETIME'); // Currently doesn't need options. - break; + break; case 'Boolean': dt = 'TINYINT(1)'; - break; + break; case 'Point': case 'GeoPoint': dt = 'POINT'; - break; + break; case 'Enum': dt = 'ENUM(' + p.type._string + ')'; dt = stringOptions(p, dt); // Enum columns can have charset/collation. - break; + break; } return dt; } function columnType(p, defaultType) { var dt = defaultType; - if(p.dataType){ + if (p.dataType) { dt = String(p.dataType); } return dt; @@ -906,24 +941,24 @@ function stringOptionsByType(p, dt) { case 'varchar': case 'char': dt += '(' + (p.limit || 255) + ')'; - break; - + break; + case 'text': case 'tinytext': case 'mediumtext': case 'longtext': - - break; + + break; } dt = stringOptions(p, dt); return dt; } -function stringOptions(p, dt){ - if(p.charset){ +function stringOptions(p, dt) { + if (p.charset) { dt += " CHARACTER SET " + p.charset; } - if(p.collation){ + if (p.collation) { dt += " COLLATE " + p.collation; } return dt; @@ -939,17 +974,17 @@ function numericOptionsByType(p, dt) { case 'integer': case 'bigint': dt = integerOptions(p, dt); - break; - + break; + case 'decimal': case 'numeric': dt = fixedPointOptions(p, dt); - break; - + break; + case 'float': case 'double': dt = floatingPointOptions(p, dt); - break; + break; } dt = unsigned(p, dt); return dt; @@ -958,17 +993,17 @@ function numericOptionsByType(p, dt) { function floatingPointOptions(p, dt) { var precision = 16; var scale = 8; - if(p.precision){ + if (p.precision) { precision = Number(p.precision); } - if(p.scale){ + if (p.scale) { scale = Number(p.scale); } if (p.precision && p.scale) { dt += '(' + precision + ',' + scale + ')'; - } else if(p.precision){ + } else if (p.precision) { dt += '(' + precision + ')'; - } + } return dt; } @@ -979,10 +1014,10 @@ function floatingPointOptions(p, dt) { function fixedPointOptions(p, dt) { var precision = 9; var scale = 2; - if(p.precision){ + if (p.precision) { precision = Number(p.precision); } - if(p.scale){ + if (p.scale) { scale = Number(p.scale); } dt += '(' + precision + ',' + scale + ')'; @@ -994,56 +1029,68 @@ function integerOptions(p, dt) { if (p.display || p.limit) { tmp = Number(p.display || p.limit); } - if(tmp > 0){ + if (tmp > 0) { dt += '(' + tmp + ')'; - } else if(p.unsigned){ + } else if (p.unsigned) { switch (dt.toLowerCase()) { default: case 'int': dt += '(10)'; - break; + break; case 'mediumint': dt += '(8)'; - break; + break; case 'smallint': dt += '(5)'; - break; + break; case 'tinyint': dt += '(3)'; - break; + break; case 'bigint': dt += '(20)'; - break; + break; } } else { switch (dt.toLowerCase()) { default: case 'int': dt += '(11)'; - break; + break; case 'mediumint': dt += '(9)'; - break; + break; case 'smallint': dt += '(6)'; - break; + break; case 'tinyint': dt += '(4)'; - break; + break; case 'bigint': dt += '(20)'; - break; + break; } } return dt; } -function unsigned(p, dt){ +function unsigned(p, dt) { if (p.unsigned) { dt += ' UNSIGNED'; } return dt; } +/** + * Disconnect from MySQL + */ +MySQL.prototype.disconnect = function () { + if(this.debug) { + console.log('disconnect'); + } + if(this.client) { + this.client.end(); + } +}; + require('./discovery')(MySQL); diff --git a/package.json b/package.json index 67b3d5b..f01f366 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback-connector-mysql", - "version": "1.0.2", + "version": "1.1.0", "description": "MySQL connector for loopback-datasource-juggler", "main": "index.js", "scripts": { diff --git a/test/connection.test.js b/test/connection.test.js index 359aaa4..fae9810 100644 --- a/test/connection.test.js +++ b/test/connection.test.js @@ -8,7 +8,7 @@ describe('migrations', function() { before(function() { require('./init.js'); - odb = getDataSource({collation: 'utf8_general_ci'}); + odb = getDataSource({collation: 'utf8_general_ci', createDatabase: true}); db = odb; }); @@ -41,7 +41,7 @@ describe('migrations', function() { }); it('should drop db and disconnect all', function(done) { - db.adapter.query('DROP DATABASE IF EXISTS ' + db.settings.database, function(err) { + db.connector.query('DROP DATABASE IF EXISTS ' + db.settings.database, function(err) { db.client.end(function(){ done(); }); @@ -57,14 +57,14 @@ function charsetTest(test_set, test_collo, test_set_str, test_set_collo, done){ assert.ok(!err); odb.client.end(function(){ - db = getSchema({collation: test_set_collo}); + db = getSchema({collation: test_set_collo, createDatabase: true}); DummyModel = db.define('DummyModel', {string: String}); db.automigrate(function(){ var q = 'SELECT DEFAULT_COLLATION_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = ' + db.client.escape(db.settings.database) + ' LIMIT 1'; - db.client.query(q, function(err, r) { + db.connector.query(q, function(err, r) { assert.ok(!err); assert.ok(r[0].DEFAULT_COLLATION_NAME.match(test_collo)); - db.client.query('SHOW VARIABLES LIKE "character_set%"', function(err, r){ + db.connector.query('SHOW VARIABLES LIKE "character_set%"', function(err, r){ assert.ok(!err); var hit_all = 0; for (var result in r) { @@ -75,7 +75,7 @@ function charsetTest(test_set, test_collo, test_set_str, test_set_collo, done){ } assert.equal(hit_all, 4); }); - db.client.query('SHOW VARIABLES LIKE "collation%"', function(err, r){ + db.connector.query('SHOW VARIABLES LIKE "collation%"', function(err, r){ assert.ok(!err); var hit_all = 0; for (var result in r) { @@ -101,7 +101,7 @@ function matchResult(result, variable_name, match) { } var query = function (sql, cb) { - odb.adapter.query(sql, cb); + odb.connector.query(sql, cb); }; diff --git a/test/init.js b/test/init.js index b455117..dac2e87 100644 --- a/test/init.js +++ b/test/init.js @@ -11,7 +11,8 @@ global.getConfig = function(options) { port: config.port || 3306, database: 'myapp_test', username: config.username, - password: config.password + password: config.password, + createDatabase: true }; if (options) {