From 8912defe8e06d30fd64e397a4683fa194693f89c Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Thu, 30 May 2013 16:06:04 -0700 Subject: [PATCH 1/4] Enhance support for composite keys --- lib/datasource.js | 31 +++++++++++++++++++++++++++---- lib/sql.js | 9 +++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/lib/datasource.js b/lib/datasource.js index ef57a4cb..fd871109 100644 --- a/lib/datasource.js +++ b/lib/datasource.js @@ -443,7 +443,7 @@ DataSource.prototype.discoverSchema = function (owner, table, cb) { pks[pk.columnName] = pk.keySeq; }); - console.log(pks); + // console.log(pks); var foreignKeys = results[2]; foreignKeys.forEach(function(fk) { @@ -455,7 +455,7 @@ DataSource.prototype.discoverSchema = function (owner, table, cb) { }; }); - console.log(fks); + // console.log(fks); if (!columns) { cb && cb(); @@ -492,6 +492,7 @@ DataSource.prototype.discoverSchema = function (owner, table, cb) { schema.properties[propName][dataSourceName] = { columnName: i.columnName, dataType: i.dataType, + dataLength: i.dataLength, nullable: i.nullable }; }); @@ -609,9 +610,9 @@ DataSource.prototype.idColumnName = function(modelName) { } /** - * Find the ID column name + * Find the ID property name * @param modelName - * @returns {String} columnName for ID + * @returns {String} property for ID */ DataSource.prototype.idName = function(modelName) { var props = this.definitions[modelName].properties; @@ -623,6 +624,28 @@ DataSource.prototype.idName = function(modelName) { return null; } +/** + * Find the ID property names sorted by the index + * @param modelName + * @returns {[String]} property names for IDs + */ +DataSource.prototype.idNames = function (modelName) { + var ids = []; + var props = this.definitions[modelName].properties; + for (var key in props) { + if (props[key].id && props[key].id > 0) { + ids.push({name: key, id: props[key].id}); + } + } + ids.sort(function (a, b) { + return a.key - b.key; + }); + var names = ids.map(function (id) { + return id.name; + }); + return names; +} + /** * Define foreign key diff --git a/lib/sql.js b/lib/sql.js index 7aed7886..5a50e9fd 100644 --- a/lib/sql.js +++ b/lib/sql.js @@ -98,6 +98,15 @@ BaseSQL.prototype.idName = function (model) { return this.dataSource(model).idName(model); }; +/** + * Get the id property names + * @param model The model name + * @returns {[String]} The id property names + */ +BaseSQL.prototype.idNames = function (model) { + return this.dataSource(model).idNames(model); +}; + /** * Get the id column name * @param model The model name From ad14bd9dbc478d5d083cf9d707ed5b3c8bc733a0 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Thu, 30 May 2013 17:23:31 -0700 Subject: [PATCH 2/4] Add more debugging info --- lib/datasource.js | 16 +++++++++++++--- lib/sql.js | 4 ++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/lib/datasource.js b/lib/datasource.js index fd871109..a5e106e5 100644 --- a/lib/datasource.js +++ b/lib/datasource.js @@ -422,6 +422,7 @@ function fromDBName(dbName, camelCase) { * @param cb */ DataSource.prototype.discoverSchema = function (owner, table, cb) { + var self = this; var dataSourceName = this.name || this.adapter.name; async.parallel( @@ -443,19 +444,28 @@ DataSource.prototype.discoverSchema = function (owner, table, cb) { pks[pk.columnName] = pk.keySeq; }); - // console.log(pks); + if(self.settings.debug) { + console.log('Primary keys: ', pks); + } var foreignKeys = results[2]; foreignKeys.forEach(function(fk) { - fks[fk.fkColumnName] = { + var fkInfo = { keySeq: fk.keySeq, owner: fk.pkOwner, tableName: fk.pkTableName, columnName: fk.pkColumnName }; + if(fks[fk.fkName]) { + fks[fk.fkName].push(fkInfo); + } else { + fks[fk.fkName] = [fkInfo]; + } }); - // console.log(fks); + if(self.settings.debug) { + console.log('Foreign keys: ', fks); + } if (!columns) { cb && cb(); diff --git a/lib/sql.js b/lib/sql.js index 5a50e9fd..895dcd05 100644 --- a/lib/sql.js +++ b/lib/sql.js @@ -29,7 +29,7 @@ BaseSQL.prototype.queryOne = function (sql, callback) { BaseSQL.prototype.dataSource = function(model) { var m = this._models[model]; if(!m) { - console.log(new Error('Model not found: ' + model).stack); + console.trace('Model not found: ' + model); } return m.model.schema; } @@ -139,7 +139,7 @@ BaseSQL.prototype.idColumnEscaped = function (model) { BaseSQL.prototype.id = function (model, prop) { var p = this._models[model].properties[prop]; if(!p) { - console.log(new Error('Property not found: ' + model +'.' + prop).stack); + console.trace('Property not found: ' + model +'.' + prop); } return p.id; }; From 2d62b5ba6a60bbccfe32aa2c862460e27fdce5c3 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Thu, 30 May 2013 23:13:04 -0700 Subject: [PATCH 3/4] Add support to discover related schemas by foreign keys --- lib/datasource.js | 190 +++++++++++++++++++++++++++++----------------- 1 file changed, 121 insertions(+), 69 deletions(-) diff --git a/lib/datasource.js b/lib/datasource.js index a5e106e5..c49b8d8d 100644 --- a/lib/datasource.js +++ b/lib/datasource.js @@ -415,100 +415,152 @@ function fromDBName(dbName, camelCase) { return parts.join(''); } +DataSource.prototype.discoverSchema = function (owner, table, cb) { + this.discoverSchemas(owner, table, {visited: {}, associations: false}, function(err, schemas) { + if(err) { + cb && cb(err, schemas); + return; + } + for(var s in schemas) { + cb && cb(null, schemas[s]); + return; + } + }); +} /** * Discover schema from a given table/view * @param owner * @param table * @param cb */ -DataSource.prototype.discoverSchema = function (owner, table, cb) { +DataSource.prototype.discoverSchemas = function (owner, table, options, cb) { var self = this; var dataSourceName = this.name || this.adapter.name; - async.parallel( - [ - this.discoverModelProperties.bind(this, owner, table), - this.discoverPrimaryKeys.bind(this, owner, table), - this.discoverForeignKeys.bind(this, owner, table) - ], function (err, results) { + var tasks = [ + this.discoverModelProperties.bind(this, owner, table), + this.discoverPrimaryKeys.bind(this, owner, table) ]; - if (err) { - cb && cb(err); - return; + if (options.associations) { + tasks.push(this.discoverForeignKeys.bind(this, owner, table)); + } + + async.parallel(tasks, function (err, results) { + + if (err) { + cb && cb(err); + return; + } + + var columns = results[0]; + if (!columns) { + cb && cb(); + return; + } + + // Handle primary keys + var primaryKeys = results[1]; + var pks = {}; + primaryKeys.forEach(function (pk) { + pks[pk.columnName] = pk.keySeq; + }); + + if (self.settings.debug) { + console.log('Primary keys: ', pks); + } + + var schema = { + name: fromDBName(table, false), + options: { + idInjection: false // DO NOT add id property + }, + properties: { } + }; - var columns = results[0]; - var primaryKeys = results[1]; - var pks = {}, fks = {}; - primaryKeys.forEach(function(pk) { - pks[pk.columnName] = pk.keySeq; - }); + schema.options[dataSourceName] = { + schema: columns[0].owner, + table: table + }; + columns.forEach(function (item) { + var i = item; + + var propName = fromDBName(item.columnName, true); + schema.properties[propName] = { + type: item.type, + required: (item.nullable === 'N'), + length: item.dataLength + }; + + if (pks[item.columnName]) { + schema.properties[propName].id = pks[item.columnName]; + } + schema.properties[propName][dataSourceName] = { + columnName: i.columnName, + dataType: i.dataType, + dataLength: i.dataLength, + nullable: i.nullable + }; + }); + + // Add current table to the visited tables + options.visited = options.visited || {}; + var schemaKey = columns[0].owner + '.' + table; + if (!options.visited.hasOwnProperty(schemaKey)) { if(self.settings.debug) { - console.log('Primary keys: ', pks); + console.log('Adding schema for ' + schemaKey); } + options.visited[schemaKey] = schema; + } + var otherTables = {}; + if (options.associations) { + // Handle foreign keys + var fks = {}; var foreignKeys = results[2]; - foreignKeys.forEach(function(fk) { - var fkInfo = { - keySeq: fk.keySeq, - owner: fk.pkOwner, - tableName: fk.pkTableName, - columnName: fk.pkColumnName - }; - if(fks[fk.fkName]) { - fks[fk.fkName].push(fkInfo); - } else { - fks[fk.fkName] = [fkInfo]; - } + foreignKeys.forEach(function (fk) { + var fkInfo = { + keySeq: fk.keySeq, + owner: fk.pkOwner, + tableName: fk.pkTableName, + columnName: fk.pkColumnName + }; + if (fks[fk.fkName]) { + fks[fk.fkName].push(fkInfo); + } else { + fks[fk.fkName] = [fkInfo]; + } }); - if(self.settings.debug) { + if (self.settings.debug) { console.log('Foreign keys: ', fks); } - if (!columns) { - cb && cb(); - return; - } - var schema = { - name: fromDBName(table, false), - options: { - idInjection: false // DO NOT add id property - }, - properties: { + foreignKeys.forEach(function (fk) { + var key = fk.pkOwner + '.' + fk.pkTableName; + if (!options.visited.hasOwnProperty(key) && !otherTables.hasOwnProperty(key)) { + otherTables[key] = {owner: fk.pkOwner, tableName: fk.pkTableName}; } - }; - - schema.options[dataSourceName] = { - schema: owner, - table: table - }; - columns.forEach(function (item) { - var i = item; - - - var propName = fromDBName(item.columnName, true); - schema.properties[propName] = - { - type: item.type, - required: (item.nullable === 'N'), - length: item.dataLength - }; - - if(pks[item.columnName]) { - schema.properties[propName].id = pks[item.columnName]; - } - schema.properties[propName][dataSourceName] = { - columnName: i.columnName, - dataType: i.dataType, - dataLength: i.dataLength, - nullable: i.nullable - }; }); + } - cb && cb(null, schema); - }); + if (Object.keys(otherTables).length === 0) { + cb && cb(null, options.visited); + } else { + var moreTasks = []; + for (var t in otherTables) { + if(self.settings.debug) { + console.log('Discovering related schema for ' + schemaKey); + } + moreTasks.push(DataSource.prototype.discoverSchemas.bind(self, otherTables[t].owner, otherTables[t].tableName, options)); + } + async.parallel(moreTasks, function (err, results) { + var result = results && results[0]; + cb && cb(err, result); + }); + } + }); } /** From 7b861c2aacb74b49c28a1e4b92ef0fe4cbaab829 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Fri, 31 May 2013 10:25:11 -0700 Subject: [PATCH 4/4] Update buildModels and support associations via foreign keys --- examples/load-schemas.js | 2 +- lib/datasource.js | 9 +++++++++ lib/model-builder.js | 13 ++++++------- test/adl.test.js | 14 +++++++------- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/examples/load-schemas.js b/examples/load-schemas.js index 9b1b83bb..148d5d82 100644 --- a/examples/load-schemas.js +++ b/examples/load-schemas.js @@ -16,7 +16,7 @@ function loadSchemasSync(schemaFile, dataSource) { // Read the schema JSON file var schemas = JSON.parse(fs.readFileSync(schemaFile)); - return DataSource.buildModels(dataSource, schemas); + return dataSource.buildModels(schemas); } diff --git a/lib/datasource.js b/lib/datasource.js index c49b8d8d..927254c1 100644 --- a/lib/datasource.js +++ b/lib/datasource.js @@ -538,6 +538,15 @@ DataSource.prototype.discoverSchemas = function (owner, table, options, cb) { } foreignKeys.forEach(function (fk) { + var propName = fromDBName(fk.pkTableName, true); + schema.properties[propName] = { + type: fromDBName(fk.pkTableName, false), + association: { + type: 'belongsTo', + foreignKey: fromDBName(fk.pkColumnName, true) + } + }; + var key = fk.pkOwner + '.' + fk.pkTableName; if (!options.visited.hasOwnProperty(key) && !otherTables.hasOwnProperty(key)) { otherTables[key] = {owner: fk.pkOwner, tableName: fk.pkTableName}; diff --git a/lib/model-builder.js b/lib/model-builder.js index b2455cbb..e719cb82 100644 --- a/lib/model-builder.js +++ b/lib/model-builder.js @@ -432,14 +432,13 @@ function buildSchema(name, properties, associations) { /** * Build models from schema definitions - * @param modelBuilder The model builder * @param schemas The schemas can be one of the following three formats: * 1. An array of named schema definition JSON objects * 2. A schema definition JSON object * 3. A list of property definitions (anonymous) * @returns {Object} A map of model constructors keyed by model name */ -function buildModels(modelBuilder, schemas) { +ModelBuilder.prototype.buildModels = function (schemas) { var models = {}; if (Array.isArray(schemas)) { @@ -461,15 +460,15 @@ function buildModels(modelBuilder, schemas) { for (var s in schemas) { var name = schemas[s].name; var schema = buildSchema(name, schemas[s].properties, associations); - var model = modelBuilder.define(name, schema); - models[name.toLowerCase()] = model; + var model = this.define(name, schema); + models[name] = model; } // Connect the models based on the associations for (var i = 0; i < associations.length; i++) { var association = associations[i]; - var sourceModel = models[association.source.toLowerCase()]; - var targetModel = models[association.target.toLowerCase()]; + var sourceModel = models[association.source]; + var targetModel = models[association.target]; if (sourceModel && targetModel) { if(typeof sourceModel[association.relation] === 'function') { sourceModel[association.relation](targetModel, {as: association.as}); @@ -479,5 +478,5 @@ function buildModels(modelBuilder, schemas) { return models; } -ModelBuilder.buildModels = buildModels; + diff --git a/test/adl.test.js b/test/adl.test.js index 5034f945..5a83edfd 100644 --- a/test/adl.test.js +++ b/test/adl.test.js @@ -168,23 +168,23 @@ describe('Load models from json', function () { // Read the schema JSON file var schemas = JSON.parse(fs.readFileSync(schemaFile)); - return DataSource.buildModels(dataSource, schemas); + return dataSource.buildModels(schemas); } var models = loadSchemasSync(path.join(__dirname, 'test1-schemas.json')); - models.should.have.property('anonymous'); - models.anonymous.should.have.property('modelName', 'Anonymous'); + models.should.have.property('Anonymous'); + models.Anonymous.should.have.property('modelName', 'Anonymous'); - var m1 = new models.anonymous({title: 'Test'}); + var m1 = new models.Anonymous({title: 'Test'}); m1.should.have.property('title', 'Test'); m1.should.have.property('author', 'Raymond'); models = loadSchemasSync(path.join(__dirname, 'test2-schemas.json')); - models.should.have.property('address'); - models.should.have.property('account'); - models.should.have.property('customer'); + models.should.have.property('Address'); + models.should.have.property('Account'); + models.should.have.property('Customer'); for (var s in models) { var m = models[s]; console.log(m.modelName, new m());