diff --git a/lib/datasource.js b/lib/datasource.js index 28f6378a..c57a099f 100644 --- a/lib/datasource.js +++ b/lib/datasource.js @@ -1190,7 +1190,32 @@ DataSource.prototype.discoverSchemas = function (modelName, options, cb) { } var self = this; - var schemaName = this.connector.name || this.name; + var dbType = this.connector.name || this.name; + + var nameMapper; + if (options.nameMapper === null) { + // No mapping + nameMapper = function(type, name) { + return name; + }; + } else if (typeof options.nameMapper === 'function') { + // Custom name mapper + nameMapper = options.nameMapper; + } else { + // Default name mapper + nameMapper = function mapName(type, name) { + if (type === 'table' || type === 'model') { + return fromDBName(name, false); + } else { + return fromDBName(name, true); + } + }; + } + + if (this.connector.discoverSchemas) { + // Delegate to the connector implementation + return this.connector.discoverSchemas(modelName, options, cb); + } var tasks = [ this.discoverModelProperties.bind(this, modelName, options), @@ -1215,7 +1240,7 @@ DataSource.prototype.discoverSchemas = function (modelName, options, cb) { } // Handle primary keys - var primaryKeys = results[1]; + var primaryKeys = results[1] || []; var pks = {}; primaryKeys.forEach(function (pk) { pks[pk.columnName] = pk.keySeq; @@ -1226,14 +1251,14 @@ DataSource.prototype.discoverSchemas = function (modelName, options, cb) { } var schema = { - name: fromDBName(modelName, false), + name: nameMapper('table', modelName), options: { idInjection: false // DO NOT add id property }, properties: {} }; - schema.options[schemaName] = { + schema.options[dbType] = { schema: columns[0].owner, table: modelName }; @@ -1241,7 +1266,7 @@ DataSource.prototype.discoverSchemas = function (modelName, options, cb) { columns.forEach(function (item) { var i = item; - var propName = fromDBName(item.columnName, true); + var propName = nameMapper('column', item.columnName); schema.properties[propName] = { type: item.type, required: (item.nullable === 'N' || item.nullable === 'NO' @@ -1254,7 +1279,7 @@ DataSource.prototype.discoverSchemas = function (modelName, options, cb) { if (pks[item.columnName]) { schema.properties[propName].id = pks[item.columnName]; } - schema.properties[propName][schemaName] = { + schema.properties[propName][dbType] = { columnName: i.columnName, dataType: i.dataType, dataLength: i.dataLength, @@ -1278,7 +1303,7 @@ DataSource.prototype.discoverSchemas = function (modelName, options, cb) { if (followingRelations) { // Handle foreign keys var fks = {}; - var foreignKeys = results[2]; + var foreignKeys = results[2] || []; foreignKeys.forEach(function (fk) { var fkInfo = { keySeq: fk.keySeq, @@ -1299,11 +1324,11 @@ DataSource.prototype.discoverSchemas = function (modelName, options, cb) { schema.options.relations = {}; foreignKeys.forEach(function (fk) { - var propName = fromDBName(fk.pkTableName, true); + var propName = nameMapper('column', fk.pkTableName); schema.options.relations[propName] = { - model: fromDBName(fk.pkTableName, false), + model: nameMapper('table', fk.pkTableName), type: 'belongsTo', - foreignKey: fromDBName(fk.fkColumnName, true) + foreignKey: nameMapper('column', fk.fkColumnName) }; var key = fk.pkOwner + '.' + fk.pkTableName; @@ -1349,13 +1374,21 @@ DataSource.prototype.discoverSchemas = function (modelName, options, cb) { */ DataSource.prototype.discoverSchemasSync = function (modelName, options) { var self = this; - var schemaName = this.name || this.connector.name; + var dbType = this.name || this.connector.name; var columns = this.discoverModelPropertiesSync(modelName, options); if (!columns || columns.length === 0) { return []; } + var nameMapper = options.nameMapper || function mapName(type, name) { + if (type === 'table' || type === 'model') { + return fromDBName(name, false); + } else { + return fromDBName(name, true); + } + }; + // Handle primary keys var primaryKeys = this.discoverPrimaryKeysSync(modelName, options); var pks = {}; @@ -1368,14 +1401,14 @@ DataSource.prototype.discoverSchemasSync = function (modelName, options) { } var schema = { - name: fromDBName(modelName, false), + name: nameMapper('table', modelName), options: { idInjection: false // DO NOT add id property }, properties: {} }; - schema.options[schemaName] = { + schema.options[dbType] = { schema: columns.length > 0 && columns[0].owner, table: modelName }; @@ -1383,7 +1416,7 @@ DataSource.prototype.discoverSchemasSync = function (modelName, options) { columns.forEach(function (item) { var i = item; - var propName = fromDBName(item.columnName, true); + var propName = nameMapper('column', item.columnName); schema.properties[propName] = { type: item.type, required: (item.nullable === 'N'), @@ -1395,7 +1428,7 @@ DataSource.prototype.discoverSchemasSync = function (modelName, options) { if (pks[item.columnName]) { schema.properties[propName].id = pks[item.columnName]; } - schema.properties[propName][schemaName] = { + schema.properties[propName][dbType] = { columnName: i.columnName, dataType: i.dataType, dataLength: i.dataLength, @@ -1441,11 +1474,11 @@ DataSource.prototype.discoverSchemasSync = function (modelName, options) { schema.options.relations = {}; foreignKeys.forEach(function (fk) { - var propName = fromDBName(fk.pkTableName, true); + var propName = nameMapper('column', fk.pkTableName); schema.options.relations[propName] = { - model: fromDBName(fk.pkTableName, false), + model: nameMapper('table', fk.pkTableName), type: 'belongsTo', - foreignKey: fromDBName(fk.fkColumnName, true) + foreignKey: nameMapper('column', fk.fkColumnName) }; var key = fk.pkOwner + '.' + fk.pkTableName; diff --git a/test/discovery.test.js b/test/discovery.test.js new file mode 100644 index 00000000..23ddc361 --- /dev/null +++ b/test/discovery.test.js @@ -0,0 +1,130 @@ +var jdb = require('../'); +var DataSource = jdb.DataSource; +var should = require('./init.js'); + +describe('Memory connector with mocked discovery', function() { + var ds; + + before(function() { + ds = new DataSource({connector: 'memory'}); + + var models = [{type: 'table', name: 'CUSTOMER', owner: 'STRONGLOOP'}, + {type: 'table', name: 'INVENTORY', owner: 'STRONGLOOP'}, + {type: 'table', name: 'LOCATION', owner: 'STRONGLOOP'}]; + + ds.discoverModelDefinitions = function(options, cb) { + process.nextTick(function() { + cb(null, models); + }); + }; + + var modelProperties = [{ + owner: 'STRONGLOOP', + tableName: 'INVENTORY', + columnName: 'PRODUCT_ID', + dataType: 'varchar', + dataLength: 20, + dataPrecision: null, + dataScale: null, + nullable: 0 + }, + { + owner: 'STRONGLOOP', + tableName: 'INVENTORY', + columnName: 'LOCATION_ID', + dataType: 'varchar', + dataLength: 20, + dataPrecision: null, + dataScale: null, + nullable: 0 + }, + { + owner: 'STRONGLOOP', + tableName: 'INVENTORY', + columnName: 'AVAILABLE', + dataType: 'int', + dataLength: null, + dataPrecision: 10, + dataScale: 0, + nullable: 1 + }, + { + owner: 'STRONGLOOP', + tableName: 'INVENTORY', + columnName: 'TOTAL', + dataType: 'int', + dataLength: null, + dataPrecision: 10, + dataScale: 0, + nullable: 1 + }]; + + ds.discoverModelProperties = function(modelName, options, cb) { + process.nextTick(function() { + cb(null, modelProperties); + }); + }; + }); + + it('should convert table/column names to camel cases', function(done) { + ds.discoverSchemas('INVENTORY', {}, function(err, schemas) { + if (err) return done(err); + schemas.should.have.property('STRONGLOOP.INVENTORY'); + var s = schemas['STRONGLOOP.INVENTORY']; + s.name.should.be.eql('Inventory'); + Object.keys(s.properties).should.be.eql( + ['productId', 'locationId', 'available', 'total']); + done(); + }); + }); + + it('should convert table/column names with custom mapper', function(done) { + ds.discoverSchemas('INVENTORY', { + nameMapper: function(type, name) { + // Convert all names to lower case + return name.toLowerCase(); + } + }, function(err, schemas) { + if (err) return done(err); + schemas.should.have.property('STRONGLOOP.INVENTORY'); + var s = schemas['STRONGLOOP.INVENTORY']; + s.name.should.be.eql('inventory'); + Object.keys(s.properties).should.be.eql( + ['product_id', 'location_id', 'available', 'total']); + done(); + }); + }); + + it('should not convert table/column names with null custom mapper', + function(done) { + ds.discoverSchemas('INVENTORY', {nameMapper: null}, function(err, schemas) { + if (err) return done(err); + schemas.should.have.property('STRONGLOOP.INVENTORY'); + var s = schemas['STRONGLOOP.INVENTORY']; + s.name.should.be.eql('INVENTORY'); + Object.keys(s.properties).should.be.eql( + ['PRODUCT_ID', 'LOCATION_ID', 'AVAILABLE', 'TOTAL']); + done(); + }); + }); + + it('should honor connector\'s discoverSchemas implementation', + function(done) { + var models = { + inventory: { + product: {type: 'string'}, + location: {type: 'string'} + } + }; + ds.connector.discoverSchemas = function(modelName, options, cb) { + process.nextTick(function() { + cb(null, models); + }); + }; + ds.discoverSchemas('INVENTORY', {nameMapper: null}, function(err, schemas) { + if (err) return done(err); + schemas.should.be.eql(models); + done(); + }); + }); +});