From 7e06e5d7b0a6194043de53c76d3edf3be3323a26 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Tue, 8 Oct 2013 08:49:14 -0700 Subject: [PATCH 1/7] Update package.json for the release --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 8808f04..5801d31 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,8 @@ "coverage": "mocha -r blanket -R html-cov > coverage_loopback-connector-mysql.html" }, "dependencies": { - "loopback-datasource-juggler": "git+ssh://git@github.com:strongloop/loopback-datasource-juggler.git", - "mysql": ">=2.0.0-alpha9", + "loopback-datasource-juggler": "~1.0.0", + "mysql": "~2.0.0-alpha9", "async": "~0.2.9" }, "devDependencies": { @@ -27,4 +27,4 @@ "url": "https://github.com/strongloop/loopback-connector-mysql.git" }, "license": "MIT" -} \ No newline at end of file +} From f7a410775f9d07ae608c51296dd24913024c95b2 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Mon, 14 Oct 2013 09:05:58 -0700 Subject: [PATCH 2/7] Remove unused dependency --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 5801d31..0655a3d 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,6 @@ "async": "~0.2.9" }, "devDependencies": { - "coffee-script": "~1.6.3", "should": "~1.3.0", "mocha": "~1.13.0", "blanket": "~1.1.5", From 2ce81681d69a88ea256140ef65e62fc79b0fd0c3 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Mon, 14 Oct 2013 14:32:19 -0700 Subject: [PATCH 3/7] Use the columnEscaped for names --- lib/mysql.js | 51 ++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/lib/mysql.js b/lib/mysql.js index 281d4f7..275da16 100644 --- a/lib/mysql.js +++ b/lib/mysql.js @@ -176,7 +176,7 @@ MySQL.prototype.updateOrCreate = function (model, data, callback) { var props = this._models[model].properties; Object.keys(data).forEach(function (key) { if (props[key] || mysql.id(model, key)) { - var k = '`' + key + '`'; + var k = mysql.columnEscaped(model, key); var v; if (!mysql.id(model, key)) { v = mysql.toDatabase(props[key], data[key]); @@ -203,13 +203,14 @@ MySQL.prototype.updateOrCreate = function (model, data, callback) { }; MySQL.prototype.toFields = function (model, data) { + var self = this; var fields = []; var props = this._models[model].properties; Object.keys(data).forEach(function (key) { if (props[key]) { var value = this.toDatabase(props[key], data[key]); if ('undefined' === typeof value) return; - fields.push('`' + key.replace(/\./g, '`.`') + '` = ' + value); + fields.push(self.columnEscaped(model, key) + ' = ' + value); } }.bind(this)); return fields.join(','); @@ -353,7 +354,7 @@ function buildWhere(self, model, conds) { var cs = []; Object.keys(conds).forEach(function (key) { - var keyEscaped = '`' + key.replace(/\./g, '`.`') + '`'; + var keyEscaped = self.columnEscaped(model, key); var val = self.toDatabase(props[key], conds[key]); if (conds[key] === null || conds[key] === undefined) { cs.push(keyEscaped + ' IS NULL'); @@ -402,14 +403,16 @@ function buildWhere(self, model, conds) { return 'WHERE ' + cs.join(' AND '); } -function buildOrderBy(order) { +function buildOrderBy(self, model, order) { if (typeof order === 'string') { order = [order]; } return 'ORDER BY ' + order.map(function(o) { var t = o.split(/[\s,]+/); - if (t.length === 1) return '`' + o + '`'; - return '`' + t[0] + '` ' + t[1]; + if (t.length === 1) { + return self.columnEscaped(model, o); + } + return self.columnEscaped(model, t[0]) + ' ' + t[1]; }).join(', '); } @@ -444,7 +447,7 @@ MySQL.prototype.all = function all(model, filter, callback) { } if (filter.order) { - sql += ' ' + buildOrderBy(filter.order); + sql += ' ' + buildOrderBy(self, model, filter.order); } if (filter.limit) { @@ -742,11 +745,35 @@ MySQL.prototype.alterTable = function (model, actualFields, actualIndexes, done, MySQL.prototype.propertiesSQL = function (model) { var self = this; + + var pks = this.idNames(model).map(function (i) { + return self.columnEscaped(model, i); + }); + + var sql = []; + if (pks.length === 1) { + var idName = this.idName(model); + sql.push(self.columnEscaped(model, idName) + ' INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY'); + } + Object.keys(this._models[model].properties).forEach(function (prop) { + if (self.id(model, prop) && pks.length === 1) { + return; + } + var colName = self.columnEscaped(model, prop); + sql.push(colName + ' ' + self.propertySettingsSQL(model, prop)); + }); + if (pks.length > 1) { + sql.push('PRIMARY KEY(' + pks.join(',') + ')'); + } + + /* 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) { var i = self._models[model].properties[prop].index; @@ -775,10 +802,11 @@ MySQL.prototype.singleIndexSettingsSQL = function (model, prop) { if (i.kind) { kind = i.kind; } + var columnName = this.columnEscaped(model, prop); if (kind && type) { - return (kind + ' INDEX `' + prop + '` (`' + prop + '`) ' + type); + return (kind + ' INDEX ' + columnName + ' (' + columnName + ') ' + type); } else { - return (kind + ' INDEX `' + prop + '` ' + type + ' (`' + prop + '`) '); + return (kind + ' INDEX ' + columnName + ' ' + type + ' (' + columnName + ') '); } }; @@ -793,10 +821,11 @@ MySQL.prototype.indexSettingsSQL = function (model, prop) { if (i.kind) { kind = i.kind; } + var columnName = this.columnEscaped(model, prop); if (kind && type) { - return (kind + ' INDEX `' + prop + '` (' + i.columns + ') ' + type); + return (kind + ' INDEX ' + columnName + ' (' + i.columns + ') ' + type); } else { - return (kind + ' INDEX ' + type + ' `' + prop + '` (' + i.columns + ')'); + return (kind + ' INDEX ' + type + ' ' + columnName + ' (' + i.columns + ')'); } }; From f70336844248ecbc2da7ba69a054e1b97d2bef40 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Mon, 14 Oct 2013 15:19:04 -0700 Subject: [PATCH 4/7] Update README --- README.md | 141 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 135 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 519f1f9..53151ef 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ## loopback-connector-mysql -MySQL connector for [LoopBack Data Source Juggler](http://docs.strongloop.com/loopback-datasource-juggler/). +`loopback-connector-mysql` is the MySQL connector module for [loopback-datasource-juggler](http://docs.strongloop.com/loopback-datasource-juggler/). ## Usage @@ -24,6 +24,8 @@ To use it you need `loopback-datasource-juggler`. ```javascript var DataSource = require('loopback-datasource-juggler').DataSource; var dataSource = new DataSource('mysql', { + host: 'localhost', + port: 3306, database: 'mydb', username: 'myuser', password: 'mypass' @@ -34,11 +36,50 @@ To use it you need `loopback-datasource-juggler`. to `utf8_general_ci`. The `collation` value will also be used to derive the connection charset. - + +## Data type mappings + +`loopback-connector-mysql` uses the following rules to map between JSON types and MySQL data types. + +### JSON to MySQL types + +- String/JSON: VARCHAR +- Text: TEXT +- Number: INT +- Date: DATETIME +- BOOLEAN: TINYINT(1) +- Point/GeoPoint: POINT +- Enum: ENUM + +### MySQL to JSON types + +- CHAR: String +- CHAR(1): Boolean +- VARCHAR/TINYTEXT/MEDIUMTEXT/LONGTEXT/TEXT/ENUM/SET: String +- TINYBLOB/MEDIUMBLOB/LONGBLOB/BLOB/BINARY/VARBINARY/BIT: Binary +- TINYINT/SMALLINT/INT/MEDIUMINT/YEAR/FLOAT/DOUBLE/NUMERIC/DECIMAL: Number +- DATE/TIMESTAMP/DATETIME: Date + ## Using the `dataType` field/column option with MySQL -The loopback-datasource-juggler MySQL connector now supports using the `dataType` column/property attribute to specify -what MySQL column type is used for many loopback-datasource-juggler types. +`loopback-connector-mysql` allows mapping of LoopBack model properties to MYSQL columns using the 'mysql' property of the + property definition. For example, + + "locationId":{ + "type":"String", + "required":true, + "length":20, + "mysql": + { + "columnName":"LOCATION_ID", + "dataType":"VARCHAR2", + "dataLength":20, + "nullable":"N" + } + } + +`loopback-connector-mysql` also supports using the `dataType` column/property attribute to specify what MySQL column +type is used for many loopback-datasource-juggler types. The following type-dataType combinations are supported: - Number @@ -115,7 +156,7 @@ The following type-dataType combinations are supported: MySQL data sources allow you to discover model definition information from existing mysql databases. See the following APIs: - - [dataSource.discoverModelDefinitions([username], fn)](https://github.com/strongloop/loopback#datasourcediscovermodeldefinitionsusername-fn) + - [dataSource.discoverModelDefinitions([owner], fn)](https://github.com/strongloop/loopback#datasourcediscovermodeldefinitionsusername-fn) - [dataSource.discoverSchema([owner], name, fn)](https://github.com/strongloop/loopback#datasourcediscoverschemaowner-name-fn) ### Asynchronous APIs for discovery @@ -221,7 +262,95 @@ MySQL data sources allow you to discover model definition information from exist ### Discover/build/try the models -The following example uses `discoverAndBuildModels` to discover, build and try the models: +#### Build a LDL schema by discovery + +Data sources backed by the MySQL connector can discover LDL models from the database using the `discoverSchema` API. For +example, + + dataSource.discoverSchema('INVENTORY', {owner: 'STRONGLOOP'}, function (err, schema) { + ... + } + +Here is the sample result. Please note there are 'mysql' properties in addition to the regular LDL model options and +properties. The 'mysql' objects contain the MySQL specific mappings. + + { + "name":"Inventory", + "options":{ + "idInjection":false, + "mysql":{ + "schema":"STRONGLOOP", + "table":"INVENTORY" + } + }, + "properties":{ + "productId":{ + "type":"String", + "required":false, + "length":60, + "precision":null, + "scale":null, + "id":1, + "mysql":{ + "columnName":"PRODUCT_ID", + "dataType":"varchar", + "dataLength":60, + "dataPrecision":null, + "dataScale":null, + "nullable":"NO" + } + }, + "locationId":{ + "type":"String", + "required":false, + "length":60, + "precision":null, + "scale":null, + "id":2, + "mysql":{ + "columnName":"LOCATION_ID", + "dataType":"varchar", + "dataLength":60, + "dataPrecision":null, + "dataScale":null, + "nullable":"NO" + } + }, + "available":{ + "type":"Number", + "required":false, + "length":null, + "precision":10, + "scale":0, + "mysql":{ + "columnName":"AVAILABLE", + "dataType":"int", + "dataLength":null, + "dataPrecision":10, + "dataScale":0, + "nullable":"YES" + } + }, + "total":{ + "type":"Number", + "required":false, + "length":null, + "precision":10, + "scale":0, + "mysql":{ + "columnName":"TOTAL", + "dataType":"int", + "dataLength":null, + "dataPrecision":10, + "dataScale":0, + "nullable":"YES" + } + } + } + } + +We can also discover and build model classes in one shot. The following example uses `discoverAndBuildModels` to discover, +build and try the models: dataSource.discoverAndBuildModels('INVENTORY', { owner: 'STRONGLOOP', visited: {}, associations: true}, function (err, models) { From 2a0970317a50cf3462f6f1cfd78019acc898649a Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Mon, 14 Oct 2013 15:19:36 -0700 Subject: [PATCH 5/7] Rename ADL to LDL --- test/mysql.discover.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/mysql.discover.test.js b/test/mysql.discover.test.js index 1766781..b39b983 100644 --- a/test/mysql.discover.test.js +++ b/test/mysql.discover.test.js @@ -171,8 +171,8 @@ describe('Discover model foreign keys', function () { }); }); -describe('Discover ADL schema from a table', function () { - it('should return an ADL schema for INVENTORY', function (done) { +describe('Discover LDL schema from a table', function () { + it('should return an LDL schema for INVENTORY', function (done) { db.discoverSchema('INVENTORY', {owner: 'STRONGLOOP'}, function (err, schema) { // console.log('%j', schema); assert(schema.name === 'Inventory'); From 20305878d2b3c2be25f8998bd3fffaa293f151b5 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Tue, 22 Oct 2013 11:25:17 -0700 Subject: [PATCH 6/7] Fix the property mapping for return values --- lib/mysql.js | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/lib/mysql.js b/lib/mysql.js index 275da16..b7a67d6 100644 --- a/lib/mysql.js +++ b/lib/mysql.js @@ -280,15 +280,19 @@ MySQL.prototype.toDatabase = function (prop, val) { * @returns {*} */ MySQL.prototype.fromDatabase = function (model, data) { - if (!data) return null; + if (!data) { + return null; + } var props = this._models[model].properties; - Object.keys(data).forEach(function (key) { + var json = {}; + for(var p in props) { + var key = this.column(model, p); var val = data[key]; if (typeof val === 'undefined' || val === null) { - return; + continue; } - if (props[key]) { - switch(props[key].type.name) { + if (props[p]) { + switch(props[p].type.name) { case 'Date': val = new Date(val.toString().replace(/GMT.*$/, 'GMT')); break; @@ -304,9 +308,9 @@ MySQL.prototype.fromDatabase = function (model, data) { break; } } - data[key] = val; - }); - return data; + json[p] = val; + } + return json; }; MySQL.prototype.escapeName = function (name) { @@ -428,6 +432,7 @@ function buildLimit(limit, offset) { * @param {Function} [callback] The callback function */ MySQL.prototype.all = function all(model, filter, callback) { + var self = this; // Order by id if no order is specified filter = filter || {}; if(!filter.order) { @@ -438,7 +443,6 @@ MySQL.prototype.all = function all(model, filter, callback) { } var sql = 'SELECT '+ this.getColumns(model, filter.fields) + ' FROM ' + this.tableEscaped(model); - var self = this; if (filter) { From 839d7b4563ed74942a735834a4883929bbe31ef5 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Tue, 22 Oct 2013 11:45:31 -0700 Subject: [PATCH 7/7] Fix the table_schema name --- lib/discovery.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/discovery.js b/lib/discovery.js index b238a43..e89e7d4 100644 --- a/lib/discovery.js +++ b/lib/discovery.js @@ -35,7 +35,7 @@ function mixinDiscovery(MySQL) { + ' FROM information_schema.tables WHERE table_schema=\'' + owner + '\'', 'table_schema, table_name', options); } else { sqlTables = paginateSQL('SELECT \'table\' AS "type", table_name AS "name",' - + ' SUBSTRING_INDEX(USER(), \'@\', 1) AS "owner" FROM information_schema.tables', + + ' table_schema AS "owner" FROM information_schema.tables', 'table_name', options); } return sqlTables; @@ -62,7 +62,7 @@ function mixinDiscovery(MySQL) { 'table_schema, table_name', options); } else { sqlViews = paginateSQL('SELECT \'view\' AS "type", table_name AS "name",' - + ' SUBSTRING_INDEX(USER(), \'@\', 1) AS "owner" FROM information_schema.views', + + ' table_schema AS "owner" FROM information_schema.views', 'table_name', options); } } @@ -164,7 +164,7 @@ function mixinDiscovery(MySQL) { + (table ? ' AND table_name=\'' + table + '\'' : ''), 'table_name, ordinal_position', {}); } else { - sql = paginateSQL('SELECT SUBSTRING_INDEX(USER(), \'@\', 1) AS "owner", table_name AS "tableName", column_name AS "columnName", data_type AS "dataType",' + sql = paginateSQL('SELECT table_schema AS "owner", table_name AS "tableName", column_name AS "columnName", data_type AS "dataType",' + ' character_octet_length AS "dataLength", numeric_precision AS "dataPrecision", numeric_scale AS "dataScale", is_nullable AS "nullable"' + ' FROM information_schema.columns' + (table ? ' WHERE table_name=\'' + table + '\'' : ''),