Merge pull request #2 from strongloop/feature/refactor-connector-base

Initial implementation.
This commit is contained in:
Miroslav Bajtoš 2014-06-03 08:22:54 +02:00
commit d6d348fccd
5 changed files with 596 additions and 1 deletions

2
index.js Normal file
View File

@ -0,0 +1,2 @@
exports.Connector = require('./lib/connector');
exports.SqlConnector = require('./lib/sql');

172
lib/connector.js Normal file
View File

@ -0,0 +1,172 @@
module.exports = Connector;
/**
* Base class for LooopBack connector. This is more a collection of useful
* methods for connectors than a super class
* @constructor
*/
function Connector(name, settings) {
this._models = {};
this.name = name;
this.settings = settings || {};
}
/**
* Set the relational property to indicate the backend is a relational DB
* @type {boolean}
*/
Connector.prototype.relational = false;
/**
* Get types associated with the connector
* @returns {String[]} The types for the connector
*/
Connector.prototype.getTypes = function() {
return ['db', 'nosql'];
};
/**
* Get the default data type for ID
* @returns {Function} The default type for ID
*/
Connector.prototype.getDefaultIdType = function() {
return String;
};
/**
* Get the metadata for the connector
* @returns {Object} The metadata object
* @property {String} type The type for the backend
* @property {Function} defaultIdType The default id type
* @property {Boolean} [isRelational] If the connector represents a relational database
* @property {Object} schemaForSettings The schema for settings object
*/
Connector.prototype.getMedadata = function () {
if (!this._metadata) {
this._metadata = {
types: this.getTypes(),
defaultIdType: this.getDefaultIdType(),
isRelational: this.isRelational || (this.getTypes().indexOf('rdbms') !== -1),
schemaForSettings: {}
};
}
return this._metadata;
};
/**
* Execute a command with given parameters
* @param {String} command The command such as SQL
* @param {Object[]} [params] An array of parameters
* @param {Function} [callback] The callback function
*/
Connector.prototype.execute = function (command, params, callback) {
/*jshint unused:false */
throw new Error('query method should be declared in connector');
};
/**
* Look up the data source by model name
* @param {String} model The model name
* @returns {DataSource} The data source
*/
Connector.prototype.getDataSource = function (model) {
var m = this._models[model];
if (!m) {
console.trace('Model not found: ' + model);
}
return m && m.model.dataSource;
};
/**
* Get the id property name
* @param {String} model The model name
* @returns {String} The id property name
*/
Connector.prototype.idName = function (model) {
return this.getDataSource(model).idName(model);
};
/**
* Get the id property names
* @param {String} model The model name
* @returns {[String]} The id property names
*/
Connector.prototype.idNames = function (model) {
return this.getDataSource(model).idNames(model);
};
/**
* Get the id index (sequence number, starting from 1)
* @param {String} model The model name
* @param {String} prop The property name
* @returns {Number} The id index, undefined if the property is not part
* of the primary key
*/
Connector.prototype.id = function (model, prop) {
var p = this._models[model].properties[prop];
if (!p) {
console.trace('Property not found: ' + model + '.' + prop);
}
return p.id;
};
/**
* Hook to be called by DataSource for defining a model
* @param {Object} modelDefinition The model definition
*/
Connector.prototype.define = function (modelDefinition) {
if (!modelDefinition.settings) {
modelDefinition.settings = {};
}
this._models[modelDefinition.model.modelName] = modelDefinition;
};
/**
* Hook to be called by DataSource for defining a model property
* @param {String} model The model name
* @param {String} propertyName The property name
* @param {Object} propertyDefinition The object for property metadata
*/
Connector.prototype.defineProperty = function (model, propertyName, propertyDefinition) {
this._models[model].properties[propertyName] = propertyDefinition;
};
/**
* Disconnect from the connector
*/
Connector.prototype.disconnect = function disconnect(cb) {
// NO-OP
if (cb) process.nextTick(cb);
};
/**
* Get the id value for the given model
* @param {String} model The model name
* @param {Object} data The model instance data
* @returns {*} The id value
*
*/
Connector.prototype.getIdValue = function (model, data) {
return data && data[this.idName(model)];
};
/**
* Set the id value for the given model
* @param {String} model The model name
* @param {Object} data The model instance data
* @param {*} value The id value
*
*/
Connector.prototype.setIdValue = function (model, data, value) {
if (data) {
data[this.idName(model)] = value;
}
};
Connector.prototype.getType = function () {
return this.type;
};

402
lib/sql.js Normal file
View File

@ -0,0 +1,402 @@
var util = require('util');
var async = require('async');
var assert = require('assert');
var Connector = require('./connector');
module.exports = SqlConnector;
/**
* Base class for connectors that are backed by relational databases/SQL
* @class
*/
function SqlConnector() {
Connector.apply(this, [].slice.call(arguments));
}
util.inherits(SqlConnector, Connector);
/**
* Set the relational property to indicate the backend is a relational DB
* @type {boolean}
*/
SqlConnector.prototype.relational = true;
/**
* Get types associated with the connector
* Returns {String[]} The types for the connector
*/
SqlConnector.prototype.getTypes = function() {
return ['db', 'rdbms', 'sql'];
};
/*!
* Get the default data type for ID
* Returns {Function}
*/
SqlConnector.prototype.getDefaultIdType = function() {
return Number;
};
SqlConnector.prototype.query = function () {
throw new Error('query method should be declared in connector');
};
SqlConnector.prototype.command = function (sql, params, callback) {
return this.query(sql, params, callback);
};
SqlConnector.prototype.queryOne = function (sql, callback) {
return this.query(sql, function (err, data) {
if (err) {
return callback(err);
}
callback(err, data && data[0]);
});
};
/**
* Get the table name for a given model.
* Returns the table name (String).
* @param {String} model The model name
*/
SqlConnector.prototype.table = function (model) {
var name = this.getDataSource(model).tableName(model);
var dbName = this.dbName;
if (typeof dbName === 'function') {
name = dbName(name);
}
return name;
};
/**
* Get the column name for given model property
* @param {String} model The model name
* @param {String} property The property name
* @returns {String} The column name
*/
SqlConnector.prototype.column = function (model, property) {
var name = this.getDataSource(model).columnName(model, property);
var dbName = this.dbName;
if (typeof dbName === 'function') {
name = dbName(name);
}
return name;
};
/**
* Get the column name for given model property
* @param {String} model The model name
* @param {String} property The property name
* @returns {Object} The column metadata
*/
SqlConnector.prototype.columnMetadata = function (model, property) {
return this.getDataSource(model).columnMetadata(model, property);
};
/**
* Get the corresponding property name for a given column name
* @param {String} model The model name
* @param {String} column The column name
* @returns {String} The property name for a given column
*/
SqlConnector.prototype.propertyName = function (model, column) {
var props = this._models[model].properties;
for (var p in props) {
if (this.column(model, p) === column) {
return p;
}
}
return null;
};
/**
* Get the id column name
* @param {String} model The model name
* @returns {String} The column name
*/
SqlConnector.prototype.idColumn = function (model) {
var name = this.getDataSource(model).idColumnName(model);
var dbName = this.dbName;
if (typeof dbName === 'function') {
name = dbName(name);
}
return name;
};
/**
* Get the escaped id column name
* @param {String} model The model name
* @returns {String} the escaped id column name
*/
SqlConnector.prototype.idColumnEscaped = function (model) {
return this.escapeName(this.getDataSource(model).idColumnName(model));
};
/**
* Escape the name for the underlying database
* @param {String} name The name
*/
SqlConnector.prototype.escapeName = function (name) {
/*jshint unused:false */
throw new Error('escapeName method should be declared in connector');
};
/**
* Get the escaped table name
* @param {String} model The model name
* @returns {String} the escaped table name
*/
SqlConnector.prototype.tableEscaped = function (model) {
return this.escapeName(this.table(model));
};
/**
* Get the escaped column name for a given model property
* @param {String} model The model name
* @param {String} property The property name
* @returns {String} The escaped column name
*/
SqlConnector.prototype.columnEscaped = function (model, property) {
return this.escapeName(this.column(model, property));
};
function isIdValuePresent(idValue, callback, returningNull) {
try {
assert(idValue !== null && idValue !== undefined, 'id value is required');
return true;
} catch (err) {
process.nextTick(function () {
if(callback) callback(returningNull ? null: err);
});
return false;
}
}
/**
* Save the model instance into the backend store
* @param {String} model The model name
* @param {Object} data The model instance data
* @param {Function} callback The callback function
*/
SqlConnector.prototype.save = function (model, data, callback) {
var idName = this.getDataSource(model).idName(model);
var idValue = data[idName];
if (!isIdValuePresent(idValue, callback)) {
return;
}
idValue = this._escapeIdValue(model, idValue);
var sql = 'UPDATE ' + this.tableEscaped(model) + ' SET ' +
this.toFields(model, data) +
' WHERE ' + this.idColumnEscaped(model) + ' = ' + idValue;
this.query(sql, function (err, result) {
if (callback) callback(err, result);
});
};
/**
* Check if a model instance exists for the given id value
* @param {String} model The model name
* @param {*} id The id value
* @param {Function} callback The callback function
*/
SqlConnector.prototype.exists = function (model, id, callback) {
if (!isIdValuePresent(id, callback, true)) {
return;
}
var sql = 'SELECT 1 FROM ' +
this.tableEscaped(model) + ' WHERE ' +
this.idColumnEscaped(model) + ' = ' + this._escapeIdValue(model, id) +
' LIMIT 1';
this.query(sql, function (err, data) {
if (!callback) return;
if (err) {
callback(err);
} else {
callback(null, data.length >= 1);
}
});
};
/**
* Find a model instance by id
* @param {String} model The model name
* @param {*} id The id value
* @param {Function} callback The callback function
*/
SqlConnector.prototype.find = function find(model, id, callback) {
if (!isIdValuePresent(id, callback, true)) {
return;
}
var self = this;
var idQuery = this.idColumnEscaped(model) + ' = ' + this._escapeIdValue(model, id);
var sql = 'SELECT * FROM ' +
this.tableEscaped(model) + ' WHERE ' + idQuery + ' LIMIT 1';
this.query(sql, function (err, data) {
var result = (data && data.length >= 1) ? data[0] : null;
if (callback) callback(err, self.fromDatabase(model, result));
});
};
/**
* Delete a model instance by id value
* @param {String} model The model name
* @param {*} id The id value
* @param {Function} callback The callback function
*/
SqlConnector.prototype.delete =
SqlConnector.prototype.destroy = function destroy(model, id, callback) {
if (!isIdValuePresent(id, callback, true)) {
return;
}
var sql = 'DELETE FROM ' + this.tableEscaped(model) + ' WHERE ' +
this.idColumnEscaped(model) + ' = ' + this._escapeIdValue(model, id);
this.command(sql, function (err, result) {
if (callback) callback(err, result);
});
};
SqlConnector.prototype._escapeIdValue = function(model, idValue) {
var idProp = this.getDataSource(model).idProperty(model);
if(typeof this.toDatabase === 'function') {
return this.toDatabase(idProp, idValue);
} else {
if(idProp.type === Number) {
return idValue;
} else {
return '\'' + idValue + '\'';
}
}
};
/**
* Delete all model instances
*
* @param {String} model The model name
* @param {Function} callback The callback function
*/
SqlConnector.prototype.deleteAll =
SqlConnector.prototype.destroyAll = function destroyAll(model, callback) {
this.command('DELETE FROM ' + this.tableEscaped(model), function (err, result) {
if (callback) callback(err, result);
});
};
/**
* Count all model instances by the where filter
*
* @param {String} model The model name
* @param {Function} callback The callback function
* @param {Object} where The where clause
*/
SqlConnector.prototype.count = function count(model, callback, where) {
var self = this;
var props = this._models[model].properties;
this.queryOne('SELECT count(*) as cnt FROM ' +
this.tableEscaped(model) + ' ' + buildWhere(where), function (err, res) {
if (err) {
return callback(err);
}
callback(err, res && res.cnt);
});
function buildWhere(conds) {
var cs = [];
Object.keys(conds || {}).forEach(function (key) {
var keyEscaped = self.columnEscaped(model, key);
if (conds[key] === null) {
cs.push(keyEscaped + ' IS NULL');
} else {
cs.push(keyEscaped + ' = ' + self.toDatabase(props[key], conds[key]));
}
});
return cs.length ? ' WHERE ' + cs.join(' AND ') : '';
}
};
/**
* Update attributes for a given model instance
* @param {String} model The model name
* @param {*} id The id value
* @param {Object} data The model data instance containing all properties to be updated
* @param {Function} cb The callback function
*/
SqlConnector.prototype.updateAttributes = function updateAttrs(model, id, data, cb) {
if (!isIdValuePresent(id, cb)) {
return;
}
var idName = this.getDataSource(model).idName(model);
data[idName] = id;
this.save(model, data, cb);
};
/**
* Disconnect from the connector
*/
SqlConnector.prototype.disconnect = function disconnect() {
// No operation
};
/**
* Recreate the tables for the given models
* @param {[String]|String} [models] A model name or an array of model names,
* if not present, apply to all models defined in the connector
* @param {Function} [cb] The callback function
*/
SqlConnector.prototype.automigrate = function (models, cb) {
var self = this;
if ((!cb) && ('function' === typeof models)) {
cb = models;
models = undefined;
}
// First argument is a model name
if ('string' === typeof models) {
models = [models];
}
models = models || Object.keys(self._models);
async.each(models, function (model, callback) {
if (model in self._models) {
self.dropTable(model, function (err) {
if (err) {
// TODO(bajtos) should we abort here and call cb(err)?
// The original code in juggler ignored the error completely
console.error(err);
}
self.createTable(model, function (err, result) {
if (err) {
console.error(err);
}
callback(err, result);
});
});
}
}, cb);
};
/**
* Drop the table for the given model from the database
* @param {String} model The model name
* @param {Function} [cb] The callback function
*/
SqlConnector.prototype.dropTable = function (model, cb) {
this.command('DROP TABLE IF EXISTS ' + this.tableEscaped(model), cb);
};
/**
* Create the table for the given model
* @param {String} model The model name
* @param {Function} [cb] The callback function
*/
SqlConnector.prototype.createTable = function (model, cb) {
this.command('CREATE TABLE ' + this.tableEscaped(model) +
' (\n ' + this.propertiesSQL(model) + '\n)', cb);
};

View File

@ -13,10 +13,17 @@
},
"main": "index.js",
"scripts": {
"pretest": "jshint ."
"pretest": "jshint .",
"test": "mocha"
},
"license": {
"name": "Dual MIT/StrongLoop",
"url": "https://github.com/strongloop/loopback-connector/blob/master/LICENSE"
},
"dependencies": {
"async": "^0.9.0"
},
"devDependencies": {
"mocha": "^1.19.0"
}
}

12
test/smoke.test.js Normal file
View File

@ -0,0 +1,12 @@
var assert = require('assert');
var connector = require('../');
describe('loopback-connector', function() {
it('exports Connector', function() {
assert(connector.Connector);
});
it('exports SqlConnector', function() {
assert(connector.SqlConnector);
});
});