Merge pull request #6 from strongloop/mysql-1.0.0

Merge mysql-1.0.0 into master
This commit is contained in:
Raymond Feng 2013-10-25 11:50:12 -07:00
commit 65d81f051f
5 changed files with 196 additions and 35 deletions

139
README.md
View File

@ -1,6 +1,6 @@
## loopback-connector-mysql ## 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 ## Usage
@ -24,6 +24,8 @@ To use it you need `loopback-datasource-juggler`.
```javascript ```javascript
var DataSource = require('loopback-datasource-juggler').DataSource; var DataSource = require('loopback-datasource-juggler').DataSource;
var dataSource = new DataSource('mysql', { var dataSource = new DataSource('mysql', {
host: 'localhost',
port: 3306,
database: 'mydb', database: 'mydb',
username: 'myuser', username: 'myuser',
password: 'mypass' password: 'mypass'
@ -35,10 +37,49 @@ To use it you need `loopback-datasource-juggler`.
connection charset. 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 ## Using the `dataType` field/column option with MySQL
The loopback-datasource-juggler MySQL connector now supports using the `dataType` column/property attribute to specify `loopback-connector-mysql` allows mapping of LoopBack model properties to MYSQL columns using the 'mysql' property of the
what MySQL column type is used for many loopback-datasource-juggler types. 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: The following type-dataType combinations are supported:
- Number - 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: 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) - [dataSource.discoverSchema([owner], name, fn)](https://github.com/strongloop/loopback#datasourcediscoverschemaowner-name-fn)
### Asynchronous APIs for discovery ### 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 ### 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}, dataSource.discoverAndBuildModels('INVENTORY', { owner: 'STRONGLOOP', visited: {}, associations: true},
function (err, models) { function (err, models) {

View File

@ -35,7 +35,7 @@ function mixinDiscovery(MySQL) {
+ ' FROM information_schema.tables WHERE table_schema=\'' + owner + '\'', 'table_schema, table_name', options); + ' FROM information_schema.tables WHERE table_schema=\'' + owner + '\'', 'table_schema, table_name', options);
} else { } else {
sqlTables = paginateSQL('SELECT \'table\' AS "type", table_name AS "name",' 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); 'table_name', options);
} }
return sqlTables; return sqlTables;
@ -62,7 +62,7 @@ function mixinDiscovery(MySQL) {
'table_schema, table_name', options); 'table_schema, table_name', options);
} else { } else {
sqlViews = paginateSQL('SELECT \'view\' AS "type", table_name AS "name",' 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); 'table_name', options);
} }
} }
@ -164,7 +164,7 @@ function mixinDiscovery(MySQL) {
+ (table ? ' AND table_name=\'' + table + '\'' : ''), + (table ? ' AND table_name=\'' + table + '\'' : ''),
'table_name, ordinal_position', {}); 'table_name, ordinal_position', {});
} else { } 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"' + ' character_octet_length AS "dataLength", numeric_precision AS "dataPrecision", numeric_scale AS "dataScale", is_nullable AS "nullable"'
+ ' FROM information_schema.columns' + ' FROM information_schema.columns'
+ (table ? ' WHERE table_name=\'' + table + '\'' : ''), + (table ? ' WHERE table_name=\'' + table + '\'' : ''),

View File

@ -176,7 +176,7 @@ MySQL.prototype.updateOrCreate = function (model, data, callback) {
var props = this._models[model].properties; var props = this._models[model].properties;
Object.keys(data).forEach(function (key) { Object.keys(data).forEach(function (key) {
if (props[key] || mysql.id(model, key)) { if (props[key] || mysql.id(model, key)) {
var k = '`' + key + '`'; var k = mysql.columnEscaped(model, key);
var v; var v;
if (!mysql.id(model, key)) { if (!mysql.id(model, key)) {
v = mysql.toDatabase(props[key], data[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) { MySQL.prototype.toFields = function (model, data) {
var self = this;
var fields = []; var fields = [];
var props = this._models[model].properties; var props = this._models[model].properties;
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' === typeof value) return;
fields.push('`' + key.replace(/\./g, '`.`') + '` = ' + value); fields.push(self.columnEscaped(model, key) + ' = ' + value);
} }
}.bind(this)); }.bind(this));
return fields.join(','); return fields.join(',');
@ -279,15 +280,19 @@ MySQL.prototype.toDatabase = function (prop, val) {
* @returns {*} * @returns {*}
*/ */
MySQL.prototype.fromDatabase = function (model, data) { MySQL.prototype.fromDatabase = function (model, data) {
if (!data) return null; if (!data) {
return null;
}
var props = this._models[model].properties; 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]; var val = data[key];
if (typeof val === 'undefined' || val === null) { if (typeof val === 'undefined' || val === null) {
return; continue;
} }
if (props[key]) { if (props[p]) {
switch(props[key].type.name) { switch(props[p].type.name) {
case 'Date': case 'Date':
val = new Date(val.toString().replace(/GMT.*$/, 'GMT')); val = new Date(val.toString().replace(/GMT.*$/, 'GMT'));
break; break;
@ -303,9 +308,9 @@ MySQL.prototype.fromDatabase = function (model, data) {
break; break;
} }
} }
data[key] = val; json[p] = val;
}); }
return data; return json;
}; };
MySQL.prototype.escapeName = function (name) { MySQL.prototype.escapeName = function (name) {
@ -353,7 +358,7 @@ function buildWhere(self, model, conds) {
var cs = []; var cs = [];
Object.keys(conds).forEach(function (key) { 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]); var val = self.toDatabase(props[key], conds[key]);
if (conds[key] === null || conds[key] === undefined) { if (conds[key] === null || conds[key] === undefined) {
cs.push(keyEscaped + ' IS NULL'); cs.push(keyEscaped + ' IS NULL');
@ -402,14 +407,16 @@ function buildWhere(self, model, conds) {
return 'WHERE ' + cs.join(' AND '); return 'WHERE ' + cs.join(' AND ');
} }
function buildOrderBy(order) { function buildOrderBy(self, model, order) {
if (typeof order === 'string') { if (typeof order === 'string') {
order = [order]; order = [order];
} }
return 'ORDER BY ' + order.map(function(o) { return 'ORDER BY ' + order.map(function(o) {
var t = o.split(/[\s,]+/); var t = o.split(/[\s,]+/);
if (t.length === 1) return '`' + o + '`'; if (t.length === 1) {
return '`' + t[0] + '` ' + t[1]; return self.columnEscaped(model, o);
}
return self.columnEscaped(model, t[0]) + ' ' + t[1];
}).join(', '); }).join(', ');
} }
@ -425,6 +432,7 @@ function buildLimit(limit, offset) {
* @param {Function} [callback] The callback function * @param {Function} [callback] The callback function
*/ */
MySQL.prototype.all = function all(model, filter, callback) { MySQL.prototype.all = function all(model, filter, callback) {
var self = this;
// Order by id if no order is specified // Order by id if no order is specified
filter = filter || {}; filter = filter || {};
if(!filter.order) { if(!filter.order) {
@ -435,7 +443,6 @@ MySQL.prototype.all = function all(model, filter, callback) {
} }
var sql = 'SELECT '+ this.getColumns(model, filter.fields) + ' FROM ' + this.tableEscaped(model); var sql = 'SELECT '+ this.getColumns(model, filter.fields) + ' FROM ' + this.tableEscaped(model);
var self = this;
if (filter) { if (filter) {
@ -444,7 +451,7 @@ MySQL.prototype.all = function all(model, filter, callback) {
} }
if (filter.order) { if (filter.order) {
sql += ' ' + buildOrderBy(filter.order); sql += ' ' + buildOrderBy(self, model, filter.order);
} }
if (filter.limit) { if (filter.limit) {
@ -742,11 +749,35 @@ MySQL.prototype.alterTable = function (model, actualFields, actualIndexes, done,
MySQL.prototype.propertiesSQL = function (model) { MySQL.prototype.propertiesSQL = function (model) {
var self = this; 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']; var sql = ['`id` INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY'];
Object.keys(this._models[model].properties).forEach(function (prop) { Object.keys(this._models[model].properties).forEach(function (prop) {
if (self.id(model, prop)) return; if (self.id(model, prop)) return;
sql.push('`' + prop + '` ' + self.propertySettingsSQL(model, prop)); sql.push('`' + prop + '` ' + self.propertySettingsSQL(model, prop));
}); });
*/
// Declared in model index property indexes. // Declared in model index property indexes.
Object.keys(this._models[model].properties).forEach(function (prop) { Object.keys(this._models[model].properties).forEach(function (prop) {
var i = self._models[model].properties[prop].index; var i = self._models[model].properties[prop].index;
@ -775,10 +806,11 @@ MySQL.prototype.singleIndexSettingsSQL = function (model, prop) {
if (i.kind) { if (i.kind) {
kind = i.kind; kind = i.kind;
} }
var columnName = this.columnEscaped(model, prop);
if (kind && type) { if (kind && type) {
return (kind + ' INDEX `' + prop + '` (`' + prop + '`) ' + type); return (kind + ' INDEX ' + columnName + ' (' + columnName + ') ' + type);
} else { } else {
return (kind + ' INDEX `' + prop + '` ' + type + ' (`' + prop + '`) '); return (kind + ' INDEX ' + columnName + ' ' + type + ' (' + columnName + ') ');
} }
}; };
@ -793,10 +825,11 @@ MySQL.prototype.indexSettingsSQL = function (model, prop) {
if (i.kind) { if (i.kind) {
kind = i.kind; kind = i.kind;
} }
var columnName = this.columnEscaped(model, prop);
if (kind && type) { if (kind && type) {
return (kind + ' INDEX `' + prop + '` (' + i.columns + ') ' + type); return (kind + ' INDEX ' + columnName + ' (' + i.columns + ') ' + type);
} else { } else {
return (kind + ' INDEX ' + type + ' `' + prop + '` (' + i.columns + ')'); return (kind + ' INDEX ' + type + ' ' + columnName + ' (' + i.columns + ')');
} }
}; };

View File

@ -11,12 +11,11 @@
"coverage": "mocha -r blanket -R html-cov > coverage_loopback-connector-mysql.html" "coverage": "mocha -r blanket -R html-cov > coverage_loopback-connector-mysql.html"
}, },
"dependencies": { "dependencies": {
"loopback-datasource-juggler": "git+ssh://git@github.com:strongloop/loopback-datasource-juggler.git", "loopback-datasource-juggler": "~1.0.0",
"mysql": ">=2.0.0-alpha9", "mysql": "~2.0.0-alpha9",
"async": "~0.2.9" "async": "~0.2.9"
}, },
"devDependencies": { "devDependencies": {
"coffee-script": "~1.6.3",
"should": "~1.3.0", "should": "~1.3.0",
"mocha": "~1.13.0", "mocha": "~1.13.0",
"blanket": "~1.1.5", "blanket": "~1.1.5",

View File

@ -171,8 +171,8 @@ describe('Discover model foreign keys', function () {
}); });
}); });
describe('Discover ADL schema from a table', function () { describe('Discover LDL schema from a table', function () {
it('should return an ADL schema for INVENTORY', function (done) { it('should return an LDL schema for INVENTORY', function (done) {
db.discoverSchema('INVENTORY', {owner: 'STRONGLOOP'}, function (err, schema) { db.discoverSchema('INVENTORY', {owner: 'STRONGLOOP'}, function (err, schema) {
// console.log('%j', schema); // console.log('%j', schema);
assert(schema.name === 'Inventory'); assert(schema.name === 'Inventory');